【问题标题】:How to disambiguate this construction in a templated conversion operator?如何在模板化转换运算符中消除这种构造的歧义?
【发布时间】:2019-01-29 01:29:32
【问题描述】:

在对为什么我的代码在 GCC 上给我一个模棱两可的错误而在 Clang 上没有错误感到困惑之后,我简化了代码。如下图所示。

struct Foo
{
    // Foo(Foo&&) = delete;
    // Foo(const Foo&) = delete;
    Foo(int*) {}
};

struct Bar
{    
    template<typename T>
    operator T()
    {
        return Foo{nullptr};
    }
};

int main() { Foo f{Bar{}}; }

错误如下。

main.cpp:17:18: error: call to constructor of 'Foo' is ambiguous
int main() { Foo f{Bar{}}; }
                 ^~~~~~~~
main.cpp:1:8: note: candidate is the implicit move constructor
struct Foo
       ^
main.cpp:1:8: note: candidate is the implicit copy constructor
main.cpp:5:1: note: candidate constructor
Foo(int*) {}
^

这次我无法为 Clang 成功编译,所以我想这只是一个 Clang 错误,这是预期的行为。

当我显式删除复制和移动构造函数(即取消注释前两行代码)时,我改为得到

note: candidate constructor has been explicitly deleted

但仍然是一个错误。那么,我将如何消除这里的构造歧义呢?

请注意,我专门添加了Foo{nullptr} 而不仅仅是nullptr,但没有区别。与显式标记 Foo ctor 相同。仅当 Bar 的转换运算符被模板化时,才会出现此歧义错误。

我可以向转换运算符添加一些 SFINAE,但我不确定我会排除什么。例如,这将使它工作:

template<typename T, std::enable_if_t<std::is_same<T, Foo>{}>* = nullptr>

这是我找到的另一个,这可能是我的答案:

template<typename T, std::enable_if_t<!std::is_same<T, int*>{}>* = nullptr> 

【问题讨论】:

  • 为什么转换运算符需要是模板?为什么不只是operator Foo()
  • @Justin,因为它是一个插图(MCVE)而不是实际问题?
  • 因为Foo 不是我将其投射到的唯一类型。这个模板化的转换运算符提供了非常好的语义(参见 nlohmann/json)。
  • 在这个例子中你真正希望被称为什么?您的 Bar 可以同时转换为...

标签: c++ templates type-conversion ambiguous


【解决方案1】:

要解决歧义,请将explicit 添加到转换运算符声明中:

struct Bar
{    
    template<typename T>
    explicit operator T()
    {
        return Foo{nullptr}; 
    }
};

为什么有必要?因为Foo 有一个构造函数采用int*,所以operator T()operator int*() 实例化被认为是f 初始化的重载决议的一部分。见下[over.match.copy]:

1 [...] 假设 cv1 T 是被初始化对象的类型, 以T为类类型,候选函数选择如下:

  • (1.1) T 的转换构造函数是候选函数。

  • (1.2) 当初始化表达式的类型是类类型“cv S”时, S 及其基类的非显式转换函数是 考虑。 将临时对象 ([class.mem]) 初始化为 绑定到参数所在的构造函数的第一个参数 类型为“对可能有 cv 限定的T 的引用”,构造函数是 在直接初始化的上下文中使用单个参数 调用 对于“cv2 T”类型的对象,显式转换函数也是 考虑。

从 (1.2) 可以看出,初始化只考虑隐式转换函数,因此存在歧义——因为编译器无法决定是使用对 Foo 的引用构造 f,还是使用前面提到的int*(通过复制初始化的方式获得)来自operator int*的返回值。 然而,初始化表达式是一个临时对象时,我们也会考虑显式转换——但前提是它们匹配一个引用 Foo 的构造函数,我们的 “可能是 cv-qualified T,即我们的 copymove 构造函数 .这整个行为与[class.conv.fct¶2] 一致:

转换函数可能是显式的([dcl.fct.spec]),在这种情况下 它仅被视为用户定义的转换 直接初始化 ([dcl.init])。 否则,用户自定义 转换不限于在作业中使用,并且 初始化。

所以,第三次在这里说同样的话:如果它没有被标记为 explicit,没有什么可以阻止编译器尝试复制初始化 int* 到用于建筑。

【讨论】:

  • 这很好,因为它解决了构造函数问题,但它禁用了其他有用的隐式转换,例如,void fn(Foo) 不能用 fn(Bar{}) 调用,它会在需要 static_cast 时调用。所以如果有办法区分构造函数和函数调用,那就太棒了
  • 好吧,我终于明白了。对于基本类型,我使用隐式转换运算符,而对于其余类型,我使用显式转换运算符。我花了一段时间,因为我把std::is_abstract 误认为std::is_fundamental
【解决方案2】:

经过一番挖掘后我的最佳猜测:我得到以下代码的相同错误:

struct Foo { Foo(int*) {} };

struct Bar {    
   operator Foo(); // { return Foo{nullptr}; }
   /* explicit */ operator int*();
};

int main() { Foo f{Bar{}}; }

而且,当我取消注释注释代码时,问题就消失了。在我看来,在 OP 的原始模板版本中,当需要从 BarFoo 的隐式转换时,GCC 只会“实例化”转换运算符声明,然后在实例化它们的主体之前解决重载。

至于为什么explicit有帮助,是因为在第二种情况下,需要多转换一次(Barint*然后int*Foo)。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2020-06-12
    • 2022-01-23
    • 2018-04-07
    • 1970-01-01
    • 1970-01-01
    • 2011-12-06
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多