Search code examples
c++templatesc++11variadic-templatesoverload-resolution

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?


Solution

  • 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.