c++ - 通用转换运算符模板和 move 语义 : any universal solution?

标签 c++ c++11 implicit-conversion move-semantics explicit-conversion

这是 Explicit ref-qualified conversion operator templates in action 的后续事件.我已经尝试了许多不同的选项,我在这里给出了一些结果,试图看看最终是否有任何解决方案。

假设一个类(例如 any)需要以一种方便、安全(毫无意外)的方式提供对任何可能类型的转换,同时保留 move 语义。我能想到四种不同的方法。

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& 重载可以在右值上调用,这会产生歧义,从而使复制初始化和隐式转换成为唯一的选择。

但是,请考虑以下不那么琐碎的类:

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 的行为类似于容器的虚假 元素,从而导致运行时错误或灾难。

最后,让我们尝试初始化一个右值引用:

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;其他编译器和版本给出的结果略有不同,同样没有通用解决方案。例如:live example .

那么:是否有可能按预期工作,或者我应该放弃转换运算符并坚持使用显式函数调用?

最佳答案

遗憾的是,C++ 标准没有任何特殊规则来解决这种特殊的歧义。问题来自您试图重载 2 个不同的东西:编译器试图转换为的类型;以及您尝试从中转换的引用类型。

通过引入代理类,您可以将分辨率拆分为 2 个步骤。第 1 步:确定它是右值引用、左值引用还是 const 左值引用。第 2 步:转换为任何类型,保留第 1 步中关于引用类型的决定。这样,您可以将解决方案与 cast() 函数一起使用,但不必指定类型:

struct A
{
    class A_r_ref
    {
        A* a_;
    public:
        A_r_ref(A* a) : a_(a) {}
        template <typename T> operator T&&() const&&;
    };

    struct A_ref
    {
        A* a_;
    public:
        A_ref(A* a) : a_(a) {}
        template <typename T> operator T&() const&&;
    };

    struct A_const_ref
    {
        const A* a_;
    public:
        A_const_ref(const A* a) : a_(a) {}
        template <typename T> operator const T&() const&&;
    };

    A_r_ref cast() && { return A_r_ref(this); }
    A_ref cast() & { return A_ref(this); }
    A_const_ref cast() const& { return A_const_ref(this); }
};

关于c++ - 通用转换运算符模板和 move 语义 : any universal solution?,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/23389672/

相关文章:

C++为什么调用这个析构函数,它从哪里来

c++ - C++中的内存管理。

c++ - #define far, #define near Windef.h

c++ - 当 double 为常量时,为什么 g++ -Wconversion 不警告将 double 转换为 long int?

c - printf() 中变量的提升和转换

scala - Scala 中 Traversable 内容的隐式转换

C++/函数中的引用

c++ - std::thread:如何等待(加入)任何给定线程完成?

c++ - 在处理回调函数时,通常的异常处理方式是什么?

c++ - 可变参数构造函数优先于用户提供的 move 构造函数,默认情况下除外