Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why variadic template constructor matches better than copy constructor?

The following code does not compile:

#include <iostream>
#include <utility>

struct Foo
{
    Foo() { std::cout << "Foo()" << std::endl; }
    Foo(int) { std::cout << "Foo(int)" << std::endl; }
};

template <typename T>
struct Bar
{
    Foo foo;

    Bar(const Bar&) { std::cout << "Bar(const Bar&)" << std::endl; }

    template <typename... Args>
    Bar(Args&&... args) : foo(std::forward<Args>(args)...)
    {
        std::cout << "Bar(Args&&... args)" << std::endl;
    }
};

int main()
{
    Bar<Foo> bar1{};
    Bar<Foo> bar2{bar1};
}

Compiler error suggest to me that compiler was trying to use variadic template constructor instead of copy constructor:

prog.cpp: In instantiation of 'Bar<T>::Bar(Args&& ...) [with Args = {Bar<Foo>&}; T = Foo]':
prog.cpp:27:20:   required from here
prog.cpp:18:55: error: no matching function for call to 'Foo::Foo(Bar<Foo>&)'
  Bar(Args&&... args) : foo(std::forward<Args>(args)...)

Why compiler does that and how to fix it?

like image 425
kpx1894 Avatar asked Jul 16 '15 21:07

kpx1894


People also ask

What is Variadic template in C++?

Variadic templates are class or function templates, that can take any variable(zero or more) number of arguments. In C++, templates can have a fixed number of parameters only that have to be specified at the time of declaration. However, variadic templates help to overcome this issue.

When copy constructor is called c++?

CPP. A copy constructor is called when a new object is created from an existing object, as a copy of the existing object. The assignment operator is called when an already initialized object is assigned a new value from another existing object.


2 Answers

This call:

Bar<Foo> bar2{bar1};

has two candidates in its overload set:

Bar(const Bar&);
Bar(Bar&);       // Args... = {Bar&}

One of the ways to determine if one conversion sequence is better than the other is, from [over.ics.rank]:

Standard conversion sequence S1 is a better conversion sequence than standard conversion sequence S2 if

— [...]
— S1 and S2 are reference bindings (8.5.3), and the types to which the references refer are the same type except for top-level cv-qualifiers, and the type to which the reference initialized by S2 refers is more cv-qualified than the type to which the reference initialized by S1 refers. [ Example:

int f(const int &);
int f(int &);
int g(const int &);
int g(int);

int i;
int j = f(i);    // calls f(int &)
int k = g(i);    // ambiguous

—end example ]

The forwarding reference variadic constructor is a better match because its reference binding (Bar&) is less cv-qualified than the copy constructor's reference binding (const Bar&).

As far as solutions, you could simply exclude from the candidate set anytime Args... is something that you should call the copy or move constructor with SFINAE:

template <typename... > struct typelist;

template <typename... Args,
          typename = std::enable_if_t<
              !std::is_same<typelist<Bar>,
                            typelist<std::decay_t<Args>...>>::value
          >>
Bar(Args&&... args)

If Args... is one of Bar, Bar&, Bar&&, const Bar&, then typelist<decay_t<Args>...> will be typelist<Bar> - and that's a case we want to exclude. Any other set of Args... will be allowed just fine.

like image 179
Barry Avatar answered Sep 25 '22 09:09

Barry


While I agree that it's counter-intuitive, the reason is that your copy constructor takes a const Bar& but bar1 is not const.

http://coliru.stacked-crooked.com/a/2622b4871d6407da

Since the universal reference can bind anything it is chosen over the more restrictive constructor with the const requirement.

like image 40
Jay Miller Avatar answered Sep 22 '22 09:09

Jay Miller