为什么可变参数模板构造函数比复制构造函数更匹配?

11
以下代码无法编译:
#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};
}

编译器错误提示我,编译器试图使用可变参数模板构造函数而不是复制构造函数:
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)...)

为什么编译器会出现这种情况,如何解决?

2
构造函数模板可以推断出对于非常量 Bar 的左值引用,这比复制构造函数更匹配。 - David G
1
由于通用构造函数需要更少的转换(即没有转换),因此更为优越。 - Kerrek SB
4个回答

12

这个调用:

Bar<Foo> bar2{bar1};

在其重载集中有两个候选项:

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

从[over.ics.rank]中得知,确定哪一个转换序列更好的一种方法是:

如果标准转换序列S1比标准转换序列S2更好,则

— [...]
— S1和S2都是引用绑定(8.5.3),并且引用所指向的类型除了顶层的cv-限定符外相同, 并且由S2初始化的引用所指向的类型比由S1初始化的引用所指向的类型更多cv-限定符。[例如:

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

转发引用可变参数构造函数更适配,因为它的引用绑定(Bar&)比复制构造函数的引用绑定(const Bar&)不带有限定符。

至于解决方案,你可以简单地将 Args... 是应该使用复制或移动构造函数调用的情况排除在候选集之外,以使用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)
如果Args...BarBar&Bar&&const Bar&之一,则typelist<decay_t<Args>...>将为typelist<Bar> - 这是我们要排除的情况。任何其他Args...都可以正常允许。

C++11版本:模板 <typename... Args, typename = typename std::enable_if< !std::is_same<typelist<Bar>, typelist<typename std::decay<Args>::type...>>::value >::type> - kpx1894
2
@kpx1894 不,C++11版本是先在你自己的命名空间中编写 decay_tenable_if_t,然后使用它们代替 std:: 版本。 - Yakk - Adam Nevraumont

6
虽然我同意这是不符合直觉的,但原因是您的复制构造函数采用了const Bar&,但bar1不是const。 http://coliru.stacked-crooked.com/a/2622b4871d6407da 由于通用引用可以绑定任何东西,因此选择它而不是更严格需要const的构造函数。

0
解决这个问题的“标准方法”是首先使用std::in_place_t参数。这样,当你需要强制编译器使用模板构造函数时,就有一个清晰的类型,并且不想要匹配时也不会让它匹配。您可以在此处https://en.cppreference.com/w/cpp/utility/optional/optional检查其操作方式。

0
避免选择可变参数构造函数的另一种方法是提供所有形式的 Bar 构造函数。
这需要更多工作,但如果您觉得 enable_if 的复杂性很重要,则可以避免它:
#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};
}

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接