【问题标题】:Why variadic template constructor matches better than copy constructor?为什么可变参数模板构造函数比复制构造函数匹配更好?
【发布时间】:2025-12-23 18:00:10
【问题描述】:

以下代码无法编译:

#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)...)

为什么编译器会这样做以及如何修复它?

【问题讨论】:

标签: c++ templates c++11 variadic-templates overload-resolution


【解决方案1】:

这个电话:

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-qualified。 [示例:

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&amp;) 比复制构造函数的引用绑定 (const Bar&amp;) 的 cv 限定更少。

就解决方案而言,您可以随时从候选集中排除 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&amp;Bar&amp;&amp;const Bar&amp; 之一,那么typelist&lt;decay_t&lt;Args&gt;...&gt; 将是typelist&lt;Bar&gt; - 这是我们要排除的情况。任何其他Args... 集都将被允许。

【讨论】:

  • C++11 版本:模板 , typelist::type...>>::value >::type>
  • @kpx1894 不,C++11版本是先在自己的命名空间中写入decay_tenable_if_t,然后用它代替std::版本。
【解决方案2】:

虽然我同意这是违反直觉的,但原因是您的复制构造函数采用 const Bar&amp;bar1 不是 const。

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

由于通用引用可以绑定任何内容,因此它选择了具有 const 要求的更严格的构造函数。

【讨论】:

    【解决方案3】:

    另一种避免选择可变参数构造函数的方法是提供所有形式的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};
    }
    

    【讨论】: