我正在尝试创建一个
strong_alias
包装类型,它可以与原始类型互换,但不能与其他 strong_alias
es 互换。
#include <array>
#include <stdint.h>
template <typename T, typename tag>
requires std::is_arithmetic<T>::value
class strong_alias
{
public:
using type = T;
constexpr strong_alias() : _value{}
{
}
template <typename other>
requires std::is_arithmetic<other>::value
constexpr strong_alias(const other &set_value) : _value{T(set_value)}
{
}
constexpr T &value()
{
return _value;
}
constexpr const T &value() const
{
return _value;
}
template <typename other>
requires std::is_arithmetic<other>::value
constexpr operator other() const
{
return other(value());
}
constexpr bool operator==(const strong_alias &rhs) const
{
return value() == rhs.value();
}
constexpr strong_alias operator*(const strong_alias &rhs) const
{
return value() * rhs.value();
}
private:
T _value;
};
using A = strong_alias<int8_t, struct A_tag>;
static_assert(std::is_assignable<A, A>());
static_assert(std::is_assignable<A, int>());
static_assert(std::is_assignable<int &, A>());
using B = strong_alias<int8_t, struct B_tag>;
static_assert(not std::is_assignable<A, B>());
static_assert(not std::is_assignable<B, A>());
static_assert(A(-3) == A(3) * -1); // builds with MSVC and GCC but not Clang
int main()
{
}
上面的代码可以使用 MSVC (
/std:c++latest
) 和 GCC (-std=c++2c
) 进行编译,但不能使用 Clang (-std=c++2c
) 进行编译。 这里有编译器资源管理器演示。
哪些编译器能够正确处理这段代码?在 Clang 下使其工作的首选方法是什么?
Compiler Explorer 的 Clang 输出:
<source>:61:29: error: use of overloaded operator '*' is ambiguous (with operand types 'A' (aka 'strong_alias<signed char, A_tag>') and 'int')
61 | static_assert(A(-3) == A(3) * -1); // builds with MSVC and GCC but not Clang
| ~~~~ ^ ~~
<source>:43:25: note: candidate function
43 | constexpr strong_alias operator*(const strong_alias &rhs) const
| ^
<source>:61:29: note: built-in candidate operator*(float, int)
61 | static_assert(A(-3) == A(3) * -1); // builds with MSVC and GCC but not Clang
| ^
<source>:61:29: note: built-in candidate operator*(double, int)
<source>:61:29: note: built-in candidate operator*(long double, int)
<source>:61:29: note: built-in candidate operator*(int, int)
[...many lines omitted...]
<source>:61:29: note: built-in candidate operator*(unsigned long long, unsigned long)
<source>:61:29: note: built-in candidate operator*(unsigned long long, unsigned long long)
<source>:61:29: note: built-in candidate operator*(unsigned long long, unsigned __int128)
1 error generated.
Compiler returned: 1
首先,我们应该在重现问题的同时尽可能地简化代码。令人失望的是,OP 似乎没有做出任何尝试这样做。这是最小化版本:
#include <type_traits>
struct A
{
A(int set_value) : _value{set_value}
{
}
template <typename other>
requires std::is_arithmetic<other>::value
operator other() const
{
return other(_value);
}
A operator*(const A &rhs) const
{
return _value * rhs._value;
}
int _value = 0;
};
int main()
{
(void)(A(3) * -1);
}
现在,这个
*
可以有几种不同的解释方式。它可以是 A::operator*
,可通过第二个操作数从 int
到 A
的隐式转换来调用,也可以是内置算术 operator*
,可通过第一个操作数的隐式转换来调用操作数从 A
到算术类型(可能第二个操作数也经历到不同算术类型的隐式转换)。 Clang 可以帮助打印出所有候选人。
之所以不明确,是因为当调用
operator*
时,我们得到了与第一个参数类型的精确匹配以及与第二个参数类型的用户定义转换。当调用内置的 operator*(int, int)
时,我们得到一个用户定义的转换,其第一个参数类型与第二个参数类型完全匹配。所以这两个候选人都不比另一个更好。
叮当是正确的。至于如何编译原始代码,由于问题是由两个候选代码引起的,每个候选代码都可以通过不同的隐式转换实现,因此您的选择基本上是:
operator*
,其第一个参数类型为 const strong_alias&
,第二个参数类型为算术类型,或者 A
的转换构造函数更改为显式,或者 A::operator other
) 更改为显式,或者我无法告诉您哪个选项最适合您。这取决于您希望您的类型提供的 API。
GCC 和 MSVC 之所以不认为它含糊不清,可能与他们使用不同的算法来确定哪些内置运算符是候选者有关。在这种特殊情况下,他们只是给出了错误的答案,但更普遍的是,该标准没有就如何在其他可能涉及无限的初始候选者的情况下缩小一组相关内置候选者的范围提供足够的指导。请参阅CWG2844。