Generische Konvertierungsoperatorvorlagen und Verschiebungssemantik: Eine universelle Lösung?

Dies ist eine Fortsetzung vonExplizite ref-qualifizierte Conversion-Operator-Vorlagen in Aktion. Ich habe mit vielen verschiedenen Optionen experimentiert und gebe hier einige Ergebnisse an, um zu sehen, ob es irgendwann eine Lösung gibt.

Sagen Sie eine Klasse (z.irgendein) muss die Konvertierung in einen beliebigen Typ auf eine bequeme, sichere (keine Überraschungen) Weise ermöglichen, die die Bewegungssemantik bewahrt. Ich kann mir vier verschiedene Arten vorstellen.

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&;
};

Die problematischsten Fälle sind das Initialisieren eines Objekts oder einer R-Wert-Referenz auf ein Objekt, wenn eine temporäre oder eine R-Wert-Referenz vorliegt. Funktionsaufrufe funktionieren in allen Fällen (glaube ich), aber ich finde sie zu ausführlich:

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)

Also experimentiere ich als nächstes mit Konvertierungsoperatoren:

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)

Es scheint, dass dieconst& Eine Überladung ist für einen r-Wert aufrufbar, der Mehrdeutigkeiten ergibt, und die Kopierinitialisierung mit einer impliziten Konvertierung als einzige Option belässt.

Betrachten Sie jedoch die folgende weniger triviale Klasse:

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>;

Hier werden generische implizite oder explizite Konstruktoren bereitgestellt, je nachdem, ob die Eingabeargumente implizit oder explizit in einen bestimmten Typ konvertiert werden können. Eine solche Logik ist nicht so exotisch, z.B. einige Implementierung vonstd::tuple kann so sein. Nun wird a initialisiertD gibt

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)

Aus verschiedenen Gründen ist die einzige verfügbare Option die Direktinitialisierung mit einer impliziten Konvertierung. Genau hier setzt jedoch die implizite Konvertierung angefährlich. b könnte tatsächlich enthalten aDDies kann eine Art Container sein, aber die funktionierende Kombination ruft aufD's Konstruktor als genaue Übereinstimmung, wob benimmt sich wie eine FälschungElement des Containers, was einen Laufzeitfehler oder eine Katastrophe verursacht.

Versuchen wir abschließend, eine R-Wert-Referenz zu initialisieren:

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)

Es scheint, dass jeder Anwendungsfall seine eigenen Anforderungen hat und es keine Kombination gibt, die in allen Fällen funktionieren könnte.

Was noch schlimmer ist, alle obigen Ergebnisse sind mit Klirren 3.3; Andere Compiler und Versionen liefern leicht abweichende Ergebnisse, wiederum ohne universelle Lösung. Zum Beispiel:Live-Beispiel.

Damit:Gibt es eine Möglichkeit, dass etwas wie gewünscht funktioniert, oder sollte ich Konvertierungsoperatoren aufgeben und mich an explizite Funktionsaufrufe halten?

Antworten auf die Frage(0)

Ihre Antwort auf die Frage