kpx1894
kpx1894

Reputation: 391

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?

Upvotes: 11

Views: 1272

Answers (4)

user20507559
user20507559

Reputation: 1

The "std-way" to solve this issue is to put a parameter of std::in_place_t first. That way you have a clear type to force the compiler to use the templated constructor when you want and to not let it match when you don't want. You could check the way it is done here https://en.cppreference.com/w/cpp/utility/optional/optional.

Upvotes: 0

Richard Hodges
Richard Hodges

Reputation: 69902

Another way to avoid the variadic constructor being selected is to supply all forms of the Bar constructor.

It's a little more work, but avoids the complexity of enable_if, if that's important to you:

#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; }
    Bar(Bar&) { std::cout << "Bar(Bar&)" << std::endl; }
    Bar(Bar&&) { std::cout << "Bar(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};
}

Upvotes: 0

Barry
Barry

Reputation: 303337

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.

Upvotes: 12

Jay Miller
Jay Miller

Reputation: 2234

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.

Upvotes: 6

Related Questions