11

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?

Barry
  • 286,269
  • 29
  • 621
  • 977
max66
  • 65,235
  • 10
  • 71
  • 111
  • 2
    I didn't look too closely, but I believe this is because you are passing parameters of type `T&` and `wrapper&` rather than `T const&` and `wrapper const&`. Thus, the perfect forwarding constructor is found first. Possible solutions include disabling the perfect forwarding constructor if the other constructors match (via SFINAE), or perhaps adding a `T&` and `wrapper&` overload (I haven't seen this solution used much, but it *should* work) – Justin Aug 20 '18 at 19:46
  • 2
    Yes, variadic template constructors do bite, and in very tender parts too (!) - they get selected whenever you least expect it. The best way to deal with them in my view is to use a special tag type which would select it (much easier than enable them with sfinae tricks). Just prefix them with something like `in_place_t` (stolen from `std::variant`) or any other tag of your chosing. – SergeyA Aug 20 '18 at 19:50
  • If you change your variadiac template ctor signature to read `wrapper (const As && ... as)`, it will no longer be called in favor of your value ctor. – cplusplusrat Aug 20 '18 at 19:53
  • @Justin - you're right for `T&` (passing a `std::string const` the `T const &` constructor is called) but (I I don't understand why) for `wrapper &`. – max66 Aug 20 '18 at 19:55
  • 1
    @cplusplusrat But then it's not a perfect forwarding constructor; it would only accept rvalues (which you couldn't even (safely) move from) – Justin Aug 20 '18 at 19:58
  • @SergeyA - yes: they bite and hurt very much. Please, write the `in_place_t` proposal as asnwer; it doesn't seems to me really satisfactory but is a better-than-nothing work-around. – max66 Aug 20 '18 at 20:00
  • @cplusplusrat - thanks but, as pointed by Justin, the `const` is incompatible with perfect forwarding; anyway, your observation seems to confirm that it's a constness problem – max66 Aug 20 '18 at 20:02
  • @max66 done as requested. – SergeyA Aug 20 '18 at 20:27
  • @Justin - You were completely right also for `wrapper &` and `wrapper const &`: making `w0` constant, `w3` compile with copy constructor; it's `w4` that gives an error (I have to sleep more!). – max66 Aug 20 '18 at 20:56

3 Answers3

9

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.

Barry
  • 286,269
  • 29
  • 621
  • 977
  • First of all, thanks for your answer; you're right about `w0`, I like your `std::is_constructible` solution and I'm agree that `w0` can be emplaced. But I still don't understand why emplace contructor is preferred over `wrapper const &` for `w3` when I make `w0` `const`. – max66 Aug 20 '18 at 20:35
  • @max66 It's not. – Barry Aug 20 '18 at 20:37
  • I get the same error when I write `wrapper const w0{s0}; wrapper w3{w0};`; both from g++ 6.3.0 and clang++ 3.8.1 (ok: I'm using an old platform) – max66 Aug 20 '18 at 20:41
  • @max66 Both of those compilers correctly accept the construction of `w3` in this situation. I don't know what you're talking about? Are you sure it's `w3` that's failing and not `w4` in your original example, since `w0` is now `const`? – Barry Aug 20 '18 at 20:44
  • Sorry: I'm an Idiot: it's `w4`: now is `const` and now I can't move. Thanks again: now is all clear – max66 Aug 20 '18 at 20:50
  • I like avoiding implicit emplace with a `struct emplace_tag` as the first argument triggering it. But it may not fit the OP's problem space. And now I see SergyA suggested it in another answer. – Yakk - Adam Nevraumont Aug 21 '18 at 00:27
  • `::is_same, T>::value` should be `wrapper` not `T` no? To avoid `wrapper&`. Also what use is `wrapper(T v)` ctor, just use the emplace one. – Yakk - Adam Nevraumont Aug 21 '18 at 00:30
  • @Yakk yep, thanks. And the emplace/sink pair is intended to be mutually exclusive. Like either emplacements, or just sink. – Barry Aug 21 '18 at 01:41
  • @barry but sink is redundant and less efficient and causes overload ambiguity now. Except maybe for `({})` based ctor. – Yakk - Adam Nevraumont Aug 21 '18 at 13:11
  • @Yakk Redundant and ambiguous with what? – Barry Aug 21 '18 at 13:25
  • @Barry The emplace ctor? `T` can already be used to construct `T` via emplace. Ah, won't be ambiguous because non-template beats template when tied. – Yakk - Adam Nevraumont Aug 21 '18 at 13:29
  • @Yakk-AdamNevraumont You won't have both. Mutually exclusive. Either emplace, or sink. – Barry Aug 21 '18 at 13:42
5

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.

SergeyA
  • 61,605
  • 5
  • 78
  • 137
  • Thanks. Generally speaking, I prefer the Barry's solution. But I have found the problem inside a class that I'm writing and, for my actual problem, I think I will adopt your solution (don't change the interface of the library I'm writing). Unfortunately I'm writing a C++11 library so I can't use `std::in_place` but I'll write an internal struct. – max66 Aug 20 '18 at 21:02
1

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:

  • the object size is smaller than 16bytes (or 8 byte for MSVC ABI),
  • all member suboject are trivially copyable,
  • this wrapper is going to be passed to functions where special care is taken for the case where the argument is of a trivially copyable type and its size is lower than the previous threshold because in this case, the argument can be passed in a register (or two) by passing the argument by value!

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!

Oliv
  • 17,610
  • 1
  • 29
  • 72
  • Yes... a forwarding constructor is another option; unfortunately I'm writing a C++11 library so I can't use `std::as_const`; but to write a substitute isn't difficult. Thanks. – max66 Aug 20 '18 at 21:11