Today I've discovered that I don't understand the C++ constructor precedence rules.
Please, see the following template struct wrapper
template <typename T>
struct wrapper
{
T value;
wrapper (T const & v0) : value{v0}
{ std::cout << "value copy constructor" << std::endl; }
wrapper (T && v0) : value{std::move(v0)}
{ std::cout << "value move constructor" << std::endl; }
template <typename ... As>
wrapper (As && ... as) : value(std::forward<As>(as)...)
{ std::cout << "emplace constructor" << std::endl; }
wrapper (wrapper const & w0) : value{w0.value}
{ std::cout << "copy constructor" << std::endl; }
wrapper (wrapper && w0) : value{std::move(w0.value)}
{ std::cout << "move constructor" << std::endl; }
};
It's a simple template value wrapper with copy constructor (wrapper const &
), a move constructor (wrapper && w0
), a sort of value copy constructor (T const & v0
), a sort of move constructor (T && v0
) and a sort of template construct-in-place-the-value constructor (As && ... as
, following the example of emplace
methods for STL containers).
My intention was to use the copy or moving constructor calling with a wrapper, the value copy or move constructor passing a T
object and the template emplace constructor calling with a list of values able to construct an object of type T
.
But I don't obtain what I expected.
From the following code
std::string s0 {"a"};
wrapper<std::string> w0{s0}; // emplace constructor (?)
wrapper<std::string> w1{std::move(s0)}; // value move constructor
wrapper<std::string> w2{1u, 'b'}; // emplace constructor
//wrapper<std::string> w3{w0}; // compilation error (?)
wrapper<std::string> w4{std::move(w0)}; // move constructor
The w1
, w2
and w4
values are constructed with value move constructor, emplace constructor and move constructor (respectively) as expected.
But w0
is constructed with emplace constructor (I was expecting value copy constructor) and w3
isn't constructed at all (compilation error) because the emplace constructor is preferred but ins't a std::string
constructor that accept a wrapper<std::string>
value.
First question: what am I doing wrong?
I suppose that the w0
problem it's because s0
isn't a const
value, so the T const &
isn't an exact match.
Indeed, if I write
std::string const s1 {"a"};
wrapper<std::string> w0{s1};
I get the value copy constructor called
Second question: what I have to do to obtain what I want?
So what I have to do to make the value copy constructor (T const &
) to get the precedence over the emplace constructor (As && ...
) also with not constant T
values and, mostly, what I have to do to get the copy constructor (wrapper const &
) take the precedence constructing w3
?
There is no such thing as "constructor precedence rules," there's nothing special about constructors in terms of precedence.
The two problem cases have the same underlying rule explaining them:
wrapper<std::string> w0{s0}; // emplace constructor (?)
wrapper<std::string> w3{w0}; // compilation error (?)
For w0
, we have two candidates: the value copy constructor (which takes a std::string const&
) and the emplace constructor (which takes a std::string&
). The latter is a better match because its reference is less cv-qualified than the value copy constructor's reference (specifically [over.ics.rank]/3). A shorter version of this is:
template <typename T> void foo(T&&); // #1
void foo(int const&); // #2
int i;
foo(i); // calls #1
Similarly, for w3
, we have two candidates: the emplace constructor (which takes a wrapper&
) and the copy constructor (which takes a wrapper const&
). Again, because of the same rule, the emplace constructor is preferred. This leads to a compile error because value
isn't actually constructible from a wrapper<std::string>
.
This is why you have to be careful with forwarding references and constrain your function templates! This is Item 26 ("Avoid overloading on universal references") and Item 27 ("Familiarize yourself with alternatives to overloading on universal references") in Effective Modern C++. Bare minimum would be:
template <typename... As,
std::enable_if_t<std::is_constructible<T, As...>::value, int> = 0>
wrapper(As&&...);
This allows w3
because now there is only one candidate. The fact that w0
emplaces instead of copies shouldn't matter, the end result is the same. Really, the value copy constructor doesn't really accomplish anything anyway - you should just remove it.
I would recommend this set of constructors:
wrapper() = default;
wrapper(wrapper const&) = default;
wrapper(wrapper&&) = default;
// if you really want emplace, this way
template <typename A=T, typename... Args,
std::enable_if_t<
std::is_constructible<T, A, As...>::value &&
!std::is_same<std::decay_t<A>, wrapper>::value
, int> = 0>
wrapper(A&& a0, Args&&... args)
: value(std::forward<A>(a0), std::forward<Args>(args)...)
{ }
// otherwise, just take the sink
wrapper(T v)
: value(std::move(v))
{ }
That gets the job done with minimal fuss and confusion. Note that the emplace and sink constructors are mutually exclusive, use exactly one of them.
As OP suggested, putting my comment as an answer with some elaboration.
Due to the way overload resolution is performed and types are matched, a variadic forward-reference type of constructor will often be selected as a best match. This would happen because all const qualification will be resolved correctly and form a perfect match - for example, when binding a const reference to a non-const lvalue and such - like in your example.
One way to deal with them would be to disable (through various sfinae methods at our disposal) variadic constructor when argument list matches (albeit imperfectly) to any of other available constructors, but this is very tedious, and requires ongoing support whenever extra constructors are added.
I personally prefer a tag-based approach and use a tag type as a first argument to variadic constructor. While any tag structure would work, I tend to (lazily) steal a type from C++17 - std::in_place
. The code now becomes:
template<class... ARGS>
Constructor(std::in_place_t, ARGS&&... args)
And than called as
Constructor ctr(std::in_place, /* arguments */);
Since in my experience in the calling place the nature of constructor is always known - i.e. you will always know if you intend to call forward-reference accepting constructor or not - this solution works well for me.
As said in the comment, the problem is that the variadic template constructor takes argument by forwarding reference, so it is a better match for non const lvalue copy or const rvalue copy.
There are many way to disable it, one efficient way is to always use a tag as in_place_t
as proposed by SergeyA in its answer. The other is to disable the template constructor when it matches the signature of a copy constructor as it is proposed in the famous Effective C++ books.
In this case, I prefer to declare all possible signature for copy/move constructors (and also copy/move assignment). This way, whatever new constructor I add to the class, I will not have to think about avoiding copy construction, it is short 2 line of code, easy to read and it does not pollute the interface of other constructors:
template <typename T>
struct wrapper
{
//...
wrapper (wrapper& w0) : wrapper(as_const(w0)){}
wrapper (const wrapper && w0) : wrapper(w0){}
};
NB: this solution should not be used if one plan to use it as a volatile type, or if all the following condition are fullfilled:
If all these requirement are fulfilled, then you may think about implementing less maintainable (error prone -> next time one will modify the code) or client interface polluting solution!
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With