这是 Explicit ref-qualified conversion operator templates in action 的进一步跟进。我已经尝试了许多不同的选项,并在这里提供了一些结果,试图看看最终是否有解决方案。
假设一个类(例如any)需要以一种方便、安全(没有意外情况)的方式提供对任何可能类型的转换,并保留移动语义。我能想到四种不同的方法。
似乎每个使用情况都有自己的要求,在所有情况下都可能没有可行的组合。
更糟糕的是,上述所有结果都是在clang 3.3下得到的;其他编译器和版本给出的结果略有不同,也没有通用解决方案。例如:实时示例。
那么,是否有什么方法可以按预期运行,还是我应该放弃转换运算符并坚持显式函数调用?
假设一个类(例如any)需要以一种方便、安全(没有意外情况)的方式提供对任何可能类型的转换,并保留移动语义。我能想到四种不同的方法。
struct A
{
// explicit conversion operators (nice, safe?)
template<typename T> explicit operator T&& () &&;
template<typename T> explicit operator T& () &;
template<typename T> explicit operator const T& () const&;
// explicit member function (ugly, safe)
template<typename T> T&& cast() &&;
template<typename T> T& cast() &;
template<typename T> const T& cast() const&;
};
// explicit non-member function (ugly, safe)
template<typename T> T&& cast(A&&);
template<typename T> T& cast(A&);
template<typename T> const T& cast(const A&);
struct B
{
// implicit conversion operators (nice, dangerous)
template<typename T> operator T&& () &&;
template<typename T> operator T& () &;
template<typename T> operator const T& () const&;
};
最具问题的情况是对于一个临时对象或右值引用初始化一个对象或右值引用。函数调用在所有情况下都有效(我认为),但我觉得它们太啰嗦了:
A a;
B b;
struct C {};
C member_move = std::move(a).cast<C>(); // U1. (ugly) OK
C member_temp = A{}.cast<C>(); // (same)
C non_member_move(cast<C>(std::move(a))); // U2. (ugly) OK
C non_member_temp(cast<C>(A{})); // (same)
所以,接下来我尝试使用转换操作符:
C direct_move_expl(std::move(a)); // 1. call to constructor of C ambiguous
C direct_temp_expl(A{}); // (same)
C direct_move_impl(std::move(b)); // 2. call to constructor of C ambiguous
C direct_temp_impl(B{}); // (same)
C copy_move_expl = std::move(a); // 3. no viable conversion from A to C
C copy_temp_expl = A{}; // (same)
C copy_move_impl = std::move(b); // 4. OK
C copy_temp_impl = B{}; // (same)
似乎const&
重载可以在rvalue上调用,这会产生歧义,只留下带有隐式转换的复制初始化作为唯一选项。
然而,请考虑以下不太简单的类:
template<typename T>
struct flexi
{
static constexpr bool all() { return true; }
template<typename A, typename... B>
static constexpr bool all(A a, B... b) { return a && all(b...); }
template<typename... A>
using convert_only = typename std::enable_if<
all(std::is_convertible<A, T>{}...),
int>::type;
template<typename... A>
using explicit_only = typename std::enable_if<
!all(std::is_convertible<A, T>{}...) &&
all(std::is_constructible<T, A>{}...),
int>::type;
template<typename... A, convert_only<A...> = 0>
flexi(A&&...);
template<typename... A, explicit_only<A...> = 0>
explicit flexi(A&&...);
};
using D = flexi<int>;
它提供通用的隐式或显式构造函数,具体取决于输入参数是否可以隐式或显式转换为某种类型。这样的逻辑并不奇怪,例如 std::tuple
的某些实现可能是这样的。现在,初始化一个 D
会得到:
D direct_move_expl_flexi(std::move(a)); // F1. call to constructor of D ambiguous
D direct_temp_expl_flexi(A{}); // (same)
D direct_move_impl_flexi(std::move(b)); // F2. OK
D direct_temp_impl_flexi(B{}); // (same)
D copy_move_expl_flexi = std::move(a); // F3. no viable conversion from A to D
D copy_temp_expl_flexi = A{}; // (same)
D copy_move_impl_flexi = std::move(b); // F4. conversion from B to D ambiguous
D copy_temp_impl_flexi = B{}; // (same)
由于种种原因,唯一可用的选项是使用隐式转换进行直接初始化。但正是在这里,隐式转换是危险的。 b
实际上可能包含一个D
,它可能是一种容器,但工作组合是将D
的构造函数作为精确匹配调用,其中b
表现得像容器中的“假”元素,从而导致运行时错误或灾难。
最后,让我们尝试初始化 rvalue 引用:
D&& ref_direct_move_expl_flexi(std::move(a)); // R1. OK
D&& ref_direct_temp_expl_flexi(A{}); // (same)
D&& ref_direct_move_impl_flexi(std::move(b)); // R2. initialization of D&& from B ambiguous
D&& ref_direct_temp_impl_flexi(B{}); // (same)
D&& ref_copy_move_expl_flexi(std::move(a)); // R3. OK
D&& ref_copy_temp_expl_flexi(A{}); // (same)
D&& ref_copy_move_impl_flexi = std::move(b); // R4. initialization of D&& from B ambiguous
D&& ref_copy_temp_impl_flexi = B{}; // (same)
似乎每个使用情况都有自己的要求,在所有情况下都可能没有可行的组合。
更糟糕的是,上述所有结果都是在clang 3.3下得到的;其他编译器和版本给出的结果略有不同,也没有通用解决方案。例如:实时示例。
那么,是否有什么方法可以按预期运行,还是我应该放弃转换运算符并坚持显式函数调用?
const&&
方法感到疑惑,它可以解决const&
和&&
之间的歧义。为了安全起见,它仍然可以返回一个const&
,但我怀疑让它返回&&
(使用const_cast
)应该是安全的,因为一个const
临时变量似乎是没有意义的。 - Matthieu M._impl
)在所有情况下都能正常工作(即使有危险),那就太好了。否则,我希望至少显式转换能够与直接初始化一起使用(包括direct_
和_expl
)。如果我的表述不够清晰,抱歉。 - iavr