通用转换运算符模板和移动语义:任何通用解决方案?

16
这是 Explicit ref-qualified conversion operator templates in action 的进一步跟进。我已经尝试了许多不同的选项,并在这里提供了一些结果,试图看看最终是否有解决方案。
假设一个类(例如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下得到的;其他编译器和版本给出的结果略有不同,也没有通用解决方案。例如:实时示例
那么,是否有什么方法可以按预期运行,还是我应该放弃转换运算符并坚持显式函数调用?

1
我对一个const&&方法感到疑惑,它可以解决const&&&之间的歧义。为了安全起见,它仍然可以返回一个const&,但我怀疑让它返回&&(使用const_cast)应该是安全的,因为一个const临时变量似乎是没有意义的。 - Matthieu M.
1
如果我没记错的话,我曾经尝试过这个方法,但是没有成功。似乎它只会增加歧义。不幸的是,我现在已经放弃了转换运算符。 - iavr
@iavr 我已经读了几遍这个问题,但不幸的是我仍然无法弄清楚每种情况下所需的功能是什么。你列举的所有情况中,你想要哪一个工作? - Mark B
1
@MarkB 嗯,如果隐式转换(包括 _impl)在所有情况下都能正常工作(即使有危险),那就太好了。否则,我希望至少显式转换能够与直接初始化一起使用(包括 direct__expl)。如果我的表述不够清晰,抱歉。 - iavr
1个回答

4
很不幸,C++标准没有任何特殊规则来解决这种歧义。问题出在你试图重载两个不同的东西:编译器试图转换的类型和你试图从中转换的引用类型。
通过引入代理类,你可以将解析分为两个步骤。第一步:确定它是一个右值引用、左值引用还是一个const左值引用。第二步:转换为任何类型,保持第一步关于引用类型的决策。这样,你可以使用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); }
};

1
回答后,我终于找到了const&&的使用案例 :) - alexk7
非常有趣,谢谢。通过省略显式类型说明,cast() 甚至可以简化为 operator* 或其他形式。顺便问一下,所有转换运算符都可以使用 && 吗?你真的需要 const&& 吗?引用类型只与 A 对象、A 持有的数据类型以及要转换的类型有关,而不是代理对象本身,对吧? - iavr
1
好的,使用const&&作为代理类转换运算符的决定来自两个方面:首先,我使用右值引用,因为我希望代理类仅用作临时变量。其次,我不修改代理类以完成其任务,因此它可以是const,所以它应该是const :) 没有比这更复杂的了。 - alexk7

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接