I'm having trouble understanding why one template instantiation works in my code and another does not.
I am using this class template I created:
template <typename T>
struct Allocated {
public:
const T& get() const& {
return *ptr.get();
}
T& get() & {
return *ptr.get();
}
T&& get() && {
return std::move(*ptr.get());
}
operator T& () & {
return *ptr.get();
}
operator const T& () const & {
return *ptr.get();
}
operator const T () && {
return std::move(* ptr.get());
}
template <typename U>
Allocated(const U& u) : ptr(std::make_unique<T>(u)) {}
template <typename U>
Allocated(U&& u) : ptr(std::make_unique<T>(std::move(u))) {}
Allocated() = delete;
Allocated(const Allocated& other) : Allocated(other.get()) {};
Allocated& operator=(const Allocated& other) {
ptr = std::make_unique<T>(*other.ptr);
}
Allocated(Allocated&& other) : Allocated(other.get()) {};
Allocated& operator=(Allocated&& other) {
ptr = std::move(other.ptr);
return *this;
}
~Allocated() = default;
protected:
std::unique_ptr<T> ptr;
};
and this code works
Allocated<int> u = 7;
bool k1 = u.get() == u.get();
bool k2 = u == u; // this works fine and calls `operator T&`
while this does not
Allocated<std::string> u = "test";
bool k1 = u.get() == u.get();
bool k2 = u == u; // error here
Question1: why is operator T&
called for int and not for std::string?
Question 2: how can I fix it for any type? I do not want to define operator==
in Allocated
and it should work for other operators like Allocated<int>.operator>(Allocated<int>)
for example.
The issue with this code is that it relies on operator ==
to provide context for implicit type conversion. It works for int
because there is a built-in bool operator ==(int, int)
candidate. When compiler checks this candidate function it knows types of arguments and is able to perform an implicit type conversion by invoking Allocated::operator int const &
. However it does not work with string
because corresponding operator for string
is actually a template
(because std::string
is an alias for std::basic_string
template
) and therefore when compiler checks this candidate it does not know types of arguments and needs to deduce them first.
The following simplified examples illustrate the difference:
template <typename T>
struct Allocated
{
operator T const & () const;
};
Type of argument of operator == is known to be NotTemplate const &
, leading to successful implicit conversion.
online compiler
struct NotTemplate{};
bool operator ==(NotTemplate const &, NotTemplate const &);
Allocated<NotTemplate> not_template{};
bool not_template_cmp{not_template == not_template}; // ok
Type of argument of operator ==
is not known in advance, first Dummy
must be deduced from Allocated<Template<void>>
, which is impossible.
online compiler
template<typename Dummy>
struct Template{};
template<typename Dummy>
bool operator ==(Template<Dummy> const &, Template<Dummy> const &);
Allocated<Template<void>> _template{};
bool _template_cmp{_template == _template}; // error, no match...
Type of argument of an additional operator ==
overload is known to be Template<void>
, leading to successful implicit conversion.
online compiler
template<typename Dummy>
struct Template{};
template<typename Dummy>
bool operator ==(Template<Dummy> const &, Template<Dummy> const &);
bool operator ==(Template<void> const &, Template<void> const &);
Allocated<Template<void>> _template{};
bool _template_cmp{_template == _template}; // ok
One way to deal with this issue is to implement operator ==
(or maybe even a spaceship operator in fresh C++ standard) overloads, including those accepting T const &
is one of tne arguments, for Allocated
class (and btw it should probably check that ptr
is not null).