C++ 升级到 VS2017:关于模糊调用的错误 C2668

C++ upgrade to VS2017: error C2668 about ambiguous call

我正在将旧库从 VS2010 升级到 VS2017。我遇到了一个我设法修复的错误,但我不明白为什么修复有效。

下面我做了一个小测试,重现了VS2017中的错误。但是,如果你在 VS2010 中 运行 这个,或者如果你在 Date class 中取消注释复制构造函数,那么它工作正常。

我得到的错误:

error.cpp(115): error C2668: 'Date::Date': ambiguous call to overloaded function  
error.cpp(22): note: could be 'Date::Date(Date &&)'  
error.cpp(22): note: or       'Date::Date(const Date &)'  
error.cpp(19): note: or       'Date::Date(std::string)'  
error.cpp(18): note: or       'Date::Date(int)'  
error.cpp(115): note: while trying to match the argument list '(CVariant)'  

代码

#include "stdafx.h"  
#include <string>  
#include <memory>  

class Date  
{
public:
    Date() { date = 19000101; }  

    // Copy constructor
    // The code will not compile in VS2017 if this constructor is not there,
    // But it compiles fine in VS2010
    /*Date(const Date & dt) {
        date = dt.date;
    }*/
    explicit Date(int yyyymmdd) { date = yyyymmdd; }  
    explicit Date(std::string isodate) { date = 19000101; } // Silly  constructor, just for this example  
private:
    int date;
};
enum cvtype {
    mInt,
    mDate,
    mNone
};

class CVariant
{
public:
    CVariant() {}

    // Copy constructor
    CVariant(const CVariant& variant) {
        copy_CVariant(variant);
    }

    // Copy assignment
    CVariant& operator=(const CVariant& variant) {
        copy_CVariant(variant);
        return *this;
    }

    void copy_CVariant(const CVariant& variant)
    {
        switch (variant._type)
        {
        case mInt:
            operator=(variant.value._Int);
            break;
        case mDate:
            operator=(*variant.value.pDate);
            break;
        default:
            clear();
            break;
        }
    }

    // Other constructors
    CVariant(const Date& date_value) : _type(mNone) { operator=(date_value);}  
    CVariant(int int_value) : _type(mNone) { operator=(int_value); }

    // casting
    operator int() const {
        if (_type == mInt) return value._Int;
        else return 0;
    }
    operator Date() const {
        if (_type == mDate) return *value.pDate;
        return Date();
    }

    // Assignment
    CVariant& operator=(int int_value) {
        clear();
        _type = mInt;
        value._Int = int_value;
        return *this;
    }
    CVariant& operator=(const Date& date_value) {
        clear();
        _type = mDate;
        value.pDate = new Date(date_value);
        return *this;
    }

private:
    void clear()
    {
        if (_type == mDate)
            delete value.pDate;
    }

    union VarValue
    {
        int _Int;
        Date* pDate;
    } value;

    cvtype _type;

};

int main()
{
    Date t(20170516);
    int i(10);
    CVariant cvt(t); 
    CVariant cvi(i); 
    // The following line only works in VS2017 if  
    // you uncomment the copy constructor in the Date class   
    // This works fine in VS2010 no matter what
    Date t1(cvt); 
    // This works 
    Date t2 = cvt;
    Date t3 = cvi;
    int i1 = cvt;
    int i2 = cvi;
    Date t4(cvt.operator Date());
    Date t5 = cvt.operator Date();
    int i3 = cvi;
    return 0;
}

我相信我理解错误:当我尝试从 CVariant 创建 Date 时,有几种可能的转换,每个转换到不同的 Date 构造函数,因此调用不明确。

但是为什么添加复制构造函数可以解决这个问题?

非常感谢您的帮助!

P.S。我知道使用隐式运算符转换,尤其是算术类型转换,不是一个好主意,但我的首要任务是让这个旧库进行编译。

问题

由于调用不明确,带有和不带有显式复制构造函数的版本都不是有效的 C++ 代码。

碰巧 MSVC 编译器做了一些 "magical" 和非标准的编译它(MSVC 的共同主题)。如果您尝试任何其他主要编译器(gcc、clang、icc,请参阅实际示例 here),它们都无法编译它。 我不会依赖这种模棱两可的代码,即使它 "works",因为它可能(并且可能会)停止使用另一个编译器版本或不同的编译器。

歧义来自 C++ 对潜在隐式转换序列进行排序的方式:它总是尝试执行最少数量的隐式转换,并且最多进行一次用户定义的转换。该标准在 [class.conv].

中更详细地描述了这个过程

在您的情况下,当调用 Date t1(cvt); 时,有两种方法可以解析调用,每种方法都只需要一个用户定义的转换(没有其他转换):

  1. CVariant 转换为 int (CVariant::operator int()),然后调用 Date::Date(int).
  2. CVariantDate (CVariant::operator Date()) 的转换,然后调用(隐式)复制构造函数 Date::Date(const Date &).

解决方法

有几种方法可以解决这个问题:

  1. explicit 关键字添加到其中一个 CVariant 转换中,因此它将不再参与隐式转换。
  2. 在呼叫站点指定您想要的转换(例如 Date t1(static_cast<Date>(cvt) 使用 CVariant::operator Date())。
  3. 添加一个从CVariantDateDate::Date(const CVariant &))的转换构造函数,这将使这个构造函数不需要转换,所以编译器会更喜欢这个而不是其他两个.

如何实施选项 3

查看完整示例 here

简而言之,您需要执行以下操作:

  • forward-declare CVariant 以便在 Date
  • 中创建转换构造函数时可以使用其名称
  • 将构造函数的声明添加到 Date
  • 在定义了CVariant之后再定义构造函数,所以可以在构造函数的实现中使用CvariantDate的转换

以下是对代码的相关更改:

class CVariant;

class Date  
{
public:
    // [...]
    explicit Date(const CVariant &cvt);
    // [...]
};

class CVariant
{
    // [...]
};


Date::Date(const CVariant &cvt) : Date(cvt.operator Date()) {}