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



我有以下代码:

#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 constructedn"; 
    }
    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 argumentsn"; 
    }
    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 argumentsn"; 
    } // Note the O& in the arguments

为什么会这样?有可能修复它并避免复制吗?

编辑:使用一个通用的参考显然使它再次工作。const引用,也就是我实际想要的,也不起作用。

此外,即使将输入参数保存到一个单独的值中(避免使用右值)也不会起作用:

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

为什么会这样

在以下声明的情况下:

template <typename O>
A(O& o);

呼叫:

A{3};

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

A(int& o);

但您正在做的是,试图将一个右值3当然是)绑定到这个实例化的非常量左值引用,而这是不允许的。

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

您也可以将o类型声明为转发引用,然后将forward类型声明为x的构造函数(但对于像int这样的基元类型,这实际上根本没有必要):

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

或者,您可以将构造函数声明为接受常量左值引用(这样右值就可以被它绑定):

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

使用通用引用可以解决问题,但不幸的是,const引用(实际上是我想要的)不能解决问题。此外,即使将输入参数保存为单独的值(避免使用右值)也不会起作用

这是因为通用引用几乎总是产生精确匹配,而采用通用引用的第一个构造函数是重载解决过程中最好的可行函数。

当传递右值时,推导出的int&&const int&更适合右值。

当传递一个左值时,推导出的int&const int&更适合非常左值(比如变量x)。

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

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

例如,对于以下呼叫:

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 constructedn"; 
    }
    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 argumentsn"; 
    }
    int x;
};

DEMO

最新更新