当参数是引用时,可变参数模板构造函数选择失败

时间:2014-09-23 17:30:12

标签: c++ c++11 overloading variadic-templates universal-reference

我有以下代码:

#include <iostream>
#include <typeinfo>

template <typename T>
struct A : T {
    template <typename ...Args>
    A(Args&&... params) : T(std::forward<Args>(params)...), x(0) {
        std::cout << "Member 'x' was default constructed\n"; 
    }

    template <typename O, typename ...Args, typename = typename std::enable_if<std::is_constructible<int,O>::value>::type>
    A(O o, Args&&... params) : T(std::forward<Args>(params)...), x(o) {
        std::cout << "Member 'x' was constructed from arguments\n"; 
    }

    int x;
};

struct B{
    B(const char*) {}
};

int main() {
    A<B> a("test");
    A<B> y(3, "test");

    return 0;
}

它工作正常,并打印

Member 'x' was default constructed
Member 'x' was constructed from arguments

但是,如果第二个重载的第一个参数是引用,则突然第二个重载永远不会被采用,并且编译失败:

template <typename O, typename ...Args, typename = typename std::enable_if<std::is_constructible<int,O>::value>::type>
    A(O& o, Args&&... params) : T(std::forward<Args>(params)...), x(o) {
        std::cout << "Member 'x' was constructed from arguments\n"; 
    } // Note the O& in the arguments

这是为什么?是否可以修复它并避免复制?

编辑:使用通用参考显然使它再次起作用。 const引用,这是我真正喜欢的,也不起作用。

此外,即使将输入参数保存为单独的值(避免右值)仍然无效:

int main() {
    double x = 3.0;
    A<B> y(x, "test"); // Still not working

    return 0;
}

1 个答案:

答案 0 :(得分:8)

  

为什么会这样?

如果出现以下声明:

template <typename O>
A(O& o);
电话:

A{3};

O类型推断为int,因此您最终得到以下实例:

A(int& o);

但是你正在做的是,你正在尝试将 rvalue (其中3肯定是)绑定到这个实例化的非const左值引用,这是 not允许

  

是否可以修复它并避免复制?

您也可以将o类型声明为转发引用,然后将forward声明为x的构造函数(但对于基本类型)比如int这根本不是必需的):

template <typename O>
A(O&& o) : x{std::forward<O>(o)} {}

或者,您可以将构造函数声明为采用const左值引用(以便rvalues可以被它绑定):

template <typename O>
A(const O& o) : x{o} {}
  

使用通用引用修复了问题,但遗憾的是,const引用(实际上是我想要的)并没有。此外,即使将输入参数保存为单独的值(避免使用右值)仍然无效。

这是因为通用引用几乎总是产生完全匹配,并且采用通用引用的第一个构造函数是重载解析过程中最好的可行函数。

传递右值时,推导出的int&&与右侧const int&的匹配值更合适。

传递左值时,推导出的int&更适合非常数左值(如变量x)而不是const int&

话虽如此,这个采用通用引用的 greedy 构造函数在两种情况下都是最可行的函数,因为在实例化时:

template <typename... Args>
A(Args&&... params);

template <typename O, typename... Args>
A(const O& z, Args&&... params);

e.g。进行以下调用:

double x = 3.0;
A a(x, "test");

编译器最终得到:

A(double&, const char (&)[5]);

A(const double&, const char (&)[5]);

第一个签名是更好匹配(无需添加const资格)。

如果由于某些原因你真的想让这个O类型被模板化(现在,无论这是一个通用引用还是一个限值引用),你必须如果第一个参数可用于构造int(就像在这种条件下启用第二个参数),则从重载解析过程中禁用第一个贪婪构造函数:

template <typename T>
struct A : T
{
    template <typename Arg, typename... Args, typename = typename std::enable_if<!std::is_constructible<int, Arg>::value>::type>
    A(Arg&& param, Args&&... params) : T(std::forward<Arg>(param), std::forward<Args>(params)...), x(0) {
        std::cout << "Member 'x' was default constructed\n"; 
    }

    template <typename O, typename... Args, typename = typename std::enable_if<std::is_constructible<int, O>::value>::type>
    A(const O& o, Args&&... params) : T(std::forward<Args>(params)...), x(o) {
        std::cout << "Member 'x' was constructed from arguments\n"; 
    }

    int x;
};

DEMO