使用模板进行隐式类型转换

41

我有一个模板 class A

template <unsigned int m>
class A
{
public:
    A(int) {}
};

它有一个从int的构造函数。我有一个操作:

template<unsigned int m>
A<m> operator+(const A<m>&, const A<m>&)
{
    return A<m>(0);
}

但是当我调用:

A<3> a(4);
A<3> b = a + 5;
A<3> c = 5 + a;

我希望int可以被隐式地转换为A,但编译器会报错。

是否有一种优雅的方法可以使隐式转换生效,而不使用以下解决方案:

  • a + A<m>(5)
  • operator+<3>(a, 5)

唯一让这个工作的方法是提供一个重载(operator+)接受一个 int 的函数。 - Nim
这是之前提出的一个问题的好答案:https://dev59.com/hHE85IYBdhLWcg3wUB2z#2833761 - George Skoptsov
4
a + 5 中,5 应该变成 A<3>(5) 而不是 A<5>(5)。如果选择后者,那么 3 应该来自于模板参数。 - Seth Carnegie
1
它应该被转换为A<3>(5)。在+中的m值对于两个参数是相同的,因此它应该从第一个参数中获取。 - Seagull
1
下意识的反应:请在构造函数上使用“explicit”。隐式转换应该保留在非常特殊的情况下使用... - Matthieu M.
1
但这是一个罕见的情况,我应该使用隐式构造函数。 - Seagull
4个回答

48
解决方案已经在这个答案中展示。现在,更多关于问题的内容...
你的代码中的问题在于重载分辨率的执行方式。当考虑模板函数进行重载分辨率时,编译器将对参数执行类型推导,并提供与调用匹配的类型替换,否则它将无法应用该模板,将其从潜在候选集中移除并继续执行。此时的问题是类型推导仅推导出精确匹配(可能带有额外的const/volatile限定符)。由于匹配是精确的,编译器将不使用任何转换(除了cv之外)。
最简单的例子是std::maxstd::min函数:
unsigned int i = 0;
std::min( i, 10 );    // Error! 

类型推导会将 template <typename T> min( T const &, T const & ) 中的第一个参数推断为 unsigned,而将第二个参数推断为 int。由于它们不同,编译器将放弃这个模板函数。

答案中提出的解决方案是使用语言的一种特性,使您能够在类定义内部定义非成员友元函数。对于每个(不同的)模板实例化,模板的优势在于编译器将创建一个在命名空间级别上具有通过在友元声明中替换实例化的实际类型获得的签名的自由非模板函数:

template <typename T>
class test {
    friend test operator+( test const & lhs, test const & rhs ) {  // [1]
        return test();
    }
}
test<int> t;                                                       // [2]

在上面的示例中,编译器允许你在类作用域内添加友元函数的定义[1]。然后,在[2]实例化模板时,编译器将生成一个自由函数。
test<int> operator+( test<int> const & lhs, test<int> const & rhs ) { 
   return test<int>();
}

该函数始终被定义,无论您是否使用它(这与模板类成员函数不同,后者在需要时才实例化)。
这里的“魔法”有多个方面。第一部分是您为每个实例化类型定义非模板函数,因此您获得了通用性和同时使用此函数时重载解决能力的优势,即使参数不完全匹配。
由于它是非模板函数,编译器能够对两个参数进行隐式转换,并且您将获得预期的行为。
此外,查找时发生了不同类型的“魔法”,因为所定义的函数只能通过参数相关查找找到,除非它也在命名空间级别声明,在我们的情况下无法以通用方式完成。这可能的含义可能是好的或坏的,这取决于您如何考虑它...
因为它只能通过ADL找到,所以除非至少一个参数已经是所需类型(即执行转换到两个参数都不会使用它),否则不会考虑它。缺点是除非您实际调用它,否则无法引用该函数,这意味着您无法获得函数指针。
(有关模板友元的更多信息,请单击此处,但请注意,在这种特殊情况下,所有其他变体都无法执行隐式转换)。

1
有没有一种方法可以在类外定义这个运算符重载,并且只在类体中有一个“特殊”的友元声明?我还没有找到这样做的方法。 - onitake
1
@dribeas:我通常更喜欢将声明和实现分开。所以,如果我只想将友元声明放入类中,但函数的定义在别处,有什么办法可以做到这一点吗? - onitake
@onitake:有点晚了,但还不到3年...你不能将那个友元函数定义为外部函数,因为它对于模板的每个实例都是不同的函数。你可以做的是提供一个在类中执行实际工作的函数(比如说static test add(test const&, test const&)),并让友元函数的定义只是简单地转发:test operator+(test const& a, test const& b) { return add(a,b); }。虽然不完美,但至少类中的定义只有一行。更复杂的逻辑被隐藏在test<T>::add中。 - David Rodríguez - dribeas
这让我对情况有了很好的理解,谢谢@DavidRodríguez-dribeas!现在,我有点神秘地想知道是否有一种解决方案能够处理两个参数的转换?(或者它需要编写显式重载,并将两个参数作为A传递吗?) - Ad N
回答自己:在我的环境中,当两种类型都为 A 时可以工作(VS 16.10.13,编译为 C++17)。 - Ad N
显示剩余2条评论

23

使用模板提供运算符的每个尝试都需要至少一个第二个重载。但是您可以通过在类内定义运算符来避免这种情况:

template <unsigned int m>
class A
{
public:
  A(int) {}
  inline friend A operator+(const A& a, const A& b) { return A(0); }
};

适用于 a+55+a


对于那些不知道原因的人,可能需要更详细的解释。+1 - David Rodríguez - dribeas
但是我需要模板A<m>,因为我想禁止对具有不同m的对象进行操作。 - Seagull
3
@Seagull:在模板类A的定义中没有模板参数的A指的是A<m>而不仅仅是任何 A<x> - David Rodríguez - dribeas
@Seagull,我在这里提供了一个更长的解释,说明实际上正在发生什么以及为什么这是解决方案。请参考此答案 - David Rodríguez - dribeas

1

添加这个运算符

template<unsigned int m>
A<m> operator+(const A<m>&, const int&)
{
    return A<m>(0);
}

或者试试这个

template <unsigned int m>
class A
{
friend const A operator+(const A& a, const A& b) { return A(0); }
public:
    A(int) {}
// OR FOR UNARY
    // const A operator+(const A &a) const {return A(0);}
};


int main(){
    A<3> a(4);
    A<3> b = a + 5;
    A<3> c = 5 + a;

}


3
这不是一个优雅的解决方案,因为我还应该添加A<m>类型的operator+ (const int&,const A<m>&),并对所有其他操作进行同样处理。 - Seagull
看看我的加法答案,虽然我不知道它是否优雅 :). - Dmitriy Kachko
你还需要一个5 + a的重载。 - Seth Carnegie
谢谢,这个解决方案好多了,但是需要为5+a创建一些东西。 - Seagull
请将以下与编程相关的内容从英文翻译成中文。请在问题中明确指定仅返回翻译后的文本: - Dmitriy Kachko
@TimKachko:在编程中,二元操作符通常都是对称的(并且通常被期望为对称),因此虽然问题中未指定,但提供答案会更有礼貌。 - Matthieu M.

0
你可以尝试为你的 A 类模板添加一个额外的“策略”类型参数,以确定实际所需的转换类型。例如:
template <unsigned int m, typename ConvVal = int>
class A
{
        public:
                typedef ConvVal conv_val;

                A(ConvVal) {}
};

template<template <unsigned int, class U> class T, unsigned int m, typename U>
T<m, U> operator+(const T<m, U>&, const T<m, U>&)
{
        return T<m, U>(0);
}

template<template <unsigned int, class U> class T, unsigned int m, typename U>
T<m, U> operator+(const T<m, U>&, const typename T<m, U>::conv_val&)
{
        return T<m, U>(0);
}

template<template <unsigned int, class U> class T, unsigned int m, typename U>
T<m, U> operator+(const typename T<m, U>::conv_val&, const T<m, U>&)
{
        return T<m, U>(0);
}

int main()
{
        A<3> a(4);
        A<3> b = a + 5;

        return 0;
}

现在你的A类将接受一个额外的模板参数,默认为int类型,并定义了你允许自动转换的实际类型。你只需要重载operator+函数三次,一次是没有转换值的版本,它将采用A<m, T>类型的显式类,另外两个版本是采用转换类型的operator+。在上面的代码中,我使用更通用的类型进行了泛化,以便可以对几乎任何具有适当模板签名并定义了conv_val typedef的其他类执行此操作。


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