类型隐式转换为 int 和 string 的模糊的字符串::operator= 调用

9

给出以下程序:

#include <iostream>
#include <string>
using namespace std;
struct GenericType{
   operator string(){
      return "Hello World";
   }
   operator int(){
      return 111;
   }
   operator double(){
      return 123.4;
   }
};
int main(){
   int i = GenericType();
   string s = GenericType();
   double d = GenericType();
   cout << i << s << d << endl;
   i = GenericType();
   s = GenericType(); //This is the troublesome line
   d = GenericType();
   cout << i << s << d << endl;
}

它可以在Visual Studio 11上编译,但不能在clang或gcc上。 它的问题在于它想要从GenericType隐式转换为intchar,但也可能返回一个string,因此存在歧义(operator =(char)operator =(string)都匹配GenericType)。

然而复制构造函数正常。

我的问题是:如何在不修改main内容的情况下解决这种歧义?我需要做什么来修改GenericType以处理这种情况?


4
隐式转换是麻烦的主要来源,建议重新考虑是否真的需要这样做。 - David Rodríguez - dribeas
2
еңЁC++11дёӯпјҢжӮЁиҝҳеҸҜд»ҘдҪҝз”Ёexplicit operator int()зӯүгҖӮиҝҷеҸҜд»ҘеғҸдҪҝз”ЁgetType()еҮҪж•°дёҖж ·жңүж•Ҳең°йҳІжӯўй”ҷиҜҜпјҢеӣ дёәз”ЁжҲ·еҝ…йЎ»жҳҫејҸиҪ¬жҚўгҖӮ - chris
1
在您之前的评论中,您声称只想在赋值和初始化时执行此操作,并询问是否可以将转换限制为这两个操作。它们不能。这是转换问题的一部分,它们会在您可能不希望它们出现的情况下发生。您还提供了替代方案 template <typename T> T get();,如果您想要赋值,请考虑使用 template <typename T> void assignTo( T& ),因为这将使用户语法更友好(编译器将推断类型)。 - David Rodríguez - dribeas
David,感谢你的关心。在考虑了你刚才尝试澄清的内容后,我编辑了我的评论。我觉得你正在努力驳斥一个有效的问题。无论这个应用对你来说有什么意义,都没有关系吧?我提出了一个简明扼要的问题,希望得到一个简明扼要的答案。 - M2tM
@M2tM:这不是未定义行为,只是编译失败了。获得你想要的结果的方法是(就像David所说)使用命名或显式转换运算符。我不知道你为什么要简单地忽略他所说的,因为那确实是正确的答案。(我承认我的评论是出于烦恼而产生的) - Mooing Duck
显示剩余8条评论
4个回答

10
I believe that gcc and clang are correct. 有两个operator=重载在运行中:
string& operator=(string const& str); // (1)
string& operator=(char ch);           // (2)

这两个operator=重载都需要从类型为GenericType的参数进行用户定义转换。 (1)需要使用到转换为string(2)需要使用到转换为int,然后再进行标准转换为char
重要的是,这两个重载都需要用户定义转换。为了确定哪个转换比另一个更好,我们可以查看重载分辨率规则,具体来说是C++11 §13.3.3.2/3中的以下规则(为了清晰起见重新格式化):

如果用户定义的转换序列U1比另一个用户定义的转换序列U2更好,则U1是更好的转换序列,如果:

  1. 它们包含相同的用户定义转换函数或构造函数或聚合初始化;并且

  2. U1的第二个标准转换序列优于U2的第二个标准转换序列。

请注意,一个连接了规则的两个部分,因此两个部分都必须满足。 规则的第一部分未满足:两个用户定义的转换序列使用不同的用户定义的转换函数。
因此,两种转换都不更好,调用是模棱两可的。
[我没有关于如何修复问题而不更改main()定义的好建议。隐式转换通常不是一个好主意;它们有时非常有用,但更频繁地可能会导致重载歧义或其他奇怪的重载行为。]
有一个gcc错误报告描述了这个问题,并解决为按设计:编译器错误地诊断了模棱两可的运算符重载。

我想表明的是,不知何故Visual Studio也能正确处理这个问题:string val; val = 65; cout << (val == 'A'); 我想知道这是否是实际编译器解析差异。我怀疑你是对的,gcc和clang是正确的,我想我会向Microsoft报告一个错误并查看情况。我真的希望有某种间接技巧来处理重载,以便在直接分配给int而无需进行转换的情况下正确匹配类型,但如果类型不完全匹配,则禁止隐式转换。 - M2tM
1
https://connect.microsoft.com/VisualStudio/feedback/details/743685/std-string-assignment-should-probably-be-ambiguous-and-fail-to-compile-but-is-not#tabs - M2tM
微软已经让他们的一位开发人员确认了这个缺陷。我个人认为这很遗憾,我认为标准对于重载决策的定义可能可以更好地解决这种歧义。在涉及用户指定转换的两个调用链的情况下,导致精确结果的那一个很容易被认为是更好的(而不是需要在 pod 类型之间进行进一步转换的那一个)。 - M2tM

2
我认为GCC是错误的。在Bjarne Stroustrup的书《C++程序设计语言》中,有一整章专门讲解运算符重载。在11.4.1节中,第一段写道:
“如果存在一个赋值运算符X::operator=(Z),使得V等于Z或者V可以唯一转换为Z,则将类型为V的值分配给类X的对象是合法的。初始化同样适用。”
在你的例子中,GCC接受了“string s = GenericType();”,但拒绝了“s = GenericType();”,因此它显然没有像初始化那样处理赋值。这是我第一个发现GCC有问题的线索。
GCC报告了3个将GenericType转换为string的候选项,均在basic_string.h中。其中一个是正确的转换,一个被报告为无效,第三个导致了歧义。这是在basic_string.h中引起歧义的运算符重载:
/**
*  @brief  Set value to string of length 1.
*  @param  c  Source character.
*
*  Assigning to a character makes this string length 1 and
*  (*this)[0] == @a c.
*/
basic_string& operator=(_CharT __c) { 
    this->assign(1, __c); 
    return *this;
}

这不是一个有效的转换,因为它接受一个与传递给它的对象类型不匹配的操作数。在任何地方都没有尝试将char赋值,因此这个转换根本不应该是候选项,更不用说导致歧义了。GCC似乎在其成员中混淆了模板类型和操作数类型。

编辑:我不知道将整数赋值给字符串实际上是合法的,因为整数可以转换为char,然后可以将char赋值给字符串(尽管字符串不能初始化为char!)。GenericType定义了对int的转换,因此使得该成员成为一个有效的候选项。然而,我仍然认为这不是一个有效的转换,原因是使用这个转换会导致两个用户定义的隐式转换的赋值,首先从GenericType到int,然后从int到string。正如同一本书11.4.1所述,“只有一个级别的用户定义的隐式转换是合法的。”


这是因为将字符初始化为字符串是该规则的一个例外。用字符初始化字符串是无效的,但是将字符赋值给字符串是有效的,正如同一本书中第20.3.7节所述。 - endoalir
被比较的rvalue是字符串类型,但是这个重载的操作数需要一个char。为了使其成为可接受的候选项,char必须用字符串的值进行初始化。例如:char c = string(v)。这毫无意义!虽然你可以将char赋值给字符串,但你不能将字符串赋值给char,这是必要的转换。由于此转换无效,因此该候选项也应无效,因此不应存在任何歧义。 - endoalir
我查看了这些章节。我明白你的意思,但是GenericType可以隐式转换为int,而int可以转换为char(因为char可以转换为int)。澄清一下,我认为歧义在于两种转换之间:1)string::operator=(static_cast<char>(static_cast<int>(GenericType())))(这里显式地写出了转换,但它们在gcc中隐式发生,并匹配string::operator=(char))。2)string::operator=(static_cast<string>(GenericType()))。其中string::operator=(char)和string::operator=(const string&)是两个竞争者。 - M2tM
如果我上面的解释是正确的,那么这意味着存在编译器差异,即Visual Studio认为执行选项1需要多一个转换,而不是选项2,因此以那种方式解决了歧义,但gcc和clang没有使用该信息来解决歧义。如果是这种情况(我相信是这样的),那么其中一个是不正确的(或者可能是未定义的,但我怀疑不是)。 - M2tM
2
不,这种情况下仍然只有一个用户定义的转换。请记住,目标类型不是string,而是char,因为这是函数参数的类型。因此,有一个用户定义的转换(从GenericTypeint),然后是一个标准转换(从intchar)。我在我的答案中添加了来自C++11的确切语言,它规定了调用是模棱两可的。 - James McNellis
显示剩余3条评论

2
我的问题是:如何在不修改main的内容的情况下解决这种歧义?
创建一个名为“string”的自己的类,该类没有歧义的“operator=”,然后不要使用std库中的“operator=”。
显然,这不是一个非常好的解决方案,但它有效,并且main不需要更改。
我认为您无法通过其他方式获得所需的行为。

这是准确的,但不如我最终授予悬赏的问题有帮助。谢谢你,不过你提供了一个正确和合理的答案。 - M2tM

1
这个解决方案有效。
#include <iostream>
#include <string>
#include <type_traits>
using namespace std;
struct GenericType{

   operator string(){
      return "Hello World";
   }
   
   template <typename T, typename = std::enable_if_t <
                                    std::is_same<T, double>::value ||
                                    std::is_same<T, int>::value>>
   operator T(){
      return 123.4;
   }
};
int main(){
   int i = GenericType();
   string s = GenericType();
   double d = GenericType();
   cout << i << s << d << endl;
   i = GenericType();
   s = GenericType();
   d = GenericType();
   cout << i << s << d << endl;
}

还有一个更通用的解决方案。我认为您不需要为每种算术类型创建运算符,因为隐式转换就可以解决问题。

// ...

template <typename T, typename = std::enable_if_t 
    <std::is_arithmetic<T>::value && !std::is_same<T, char>::value>>
operator T() const
{
    return std::stod("123.4");
}

//...

非常感谢您的回答!实际上,这是唯一对我有效的解决方案。我相信,这是由于模板引入了另一层间接性,对吗? - n0dus

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