16

これは、アクション中の Explicit ref-qualified conversion operator テンプレートのフォローアップです。私はさまざまなオプションを試しましたが、最終的に解決策があるかどうかを確認するために、ここでいくつかの結果を示しています.

クラス (たとえばany ) が、移動のセマンティクスを保持する、便利で安全な (驚くことのない) 方法で、可能な型への変換を提供する必要があるとします。4つの異なる方法を考えることができます。

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

だから:何かが望みどおりに機能する可能性はありますか、それとも変換演算子をあきらめて明示的な関数呼び出しに固執する必要がありますか?

4

1 に答える 1

4

残念ながら、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); }
};
于 2014-07-02T21:31:47.653 に答える