Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

class constructor precedence with a variadic template constructor for a value wrapper

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?

like image 700
max66 Avatar asked Aug 20 '18 19:08

max66


3 Answers

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.

like image 70
Barry Avatar answered Sep 29 '22 08:09

Barry


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.

like image 22
SergeyA Avatar answered Sep 29 '22 09:09

SergeyA


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!

like image 33
Oliv Avatar answered Sep 29 '22 08:09

Oliv