std::map::emplace: GCC vs. MSVC



  • Hallo,

    ich möchte portablen Code schreiben und habe ein Problem mit map emplace():

    class TestX1
    {
    	int x;
    public:
    	TestX1(const TestX1&) = delete;
    	TestX1(int x) : x(x) {}
    	~TestX1() {};
    };
    
    int main()
    {
    	std::map<int, TestX1> m;
    
    	m.emplace(42, 42); // Variante 1: GCC error, MSVC OK
    
    	m.emplace(std::piecewise_construct, std::make_tuple(42), std::make_tuple(42)); // Variante 2: beide OK
    }
    

    Mit MSVC sind beide emplace Varianten OK.
    Doch der GCC 5.2.1 meckert bei Variante 1:

    stl_pair.h(134,45): error : use of deleted function 'TestX1::TestX1(const TestX1&)'
    : first(std::forward<_U1>(__x)), second(__y) { }

    // stl_pair.h:

    template<class _U1, class = typename
    enable_if<is_convertible<_U1, _T1>::value>::type>
    constexpr pair(_U1&& __x, const _T2& __y)
    : first(std::forward<_U1>(__x)), second(__y) { }

    Warum ist das so? Was macht GCC anders? Muss ich jetzt überall Variante 2 nehmen, oder gibt es andere Möglichkeiten?

    Dankesehr!



  • Microsoft machte schon immer "mehr" und manchmal auch bissi anders als der Standard ...
    Am besten Du schaust im aktuellen Standard nach, was gehen muss/soll.
    Dann schausst ob MS und die anderen das so akzeptieren.

    Wenn es keinen gemeinsammen Nenner gibt, musst mit compiler-weichen arbeiten, also auf preprozessor ebene das system abfragen und zum richtigeh code verzweigen.

    Hier noch mal die überisicht, wo draus vorgehen sollte, das der gcc doch recht nahe am Standard ist.
    Wobei der Standard das minimum ist, mehr zerstört nicht die standard Konformität sondern macht nur abhängig ^^
    http://en.cppreference.com/w/cpp/compiler_support

    Ciao ...



  • RHBaum schrieb:

    Am besten Du schaust im aktuellen Standard nach, was gehen muss/soll.

    Für jede kleine Funktion? 🤡
    Könnte es theoretisch auch zu Laufzeitproblemen kommen (allgemein)? Das wäre ja 👎

    Habe ich in meinem Fall nur die make_tuple Möglichkeit?



  • ReEval schrieb:

    Warum ist das so? Was macht GCC anders? Muss ich jetzt überall Variante 2 nehmen, oder gibt es andere Möglichkeiten?

    Du könntest TestX1 einen move-constructor verpassen.



  • Das wäre im Standard C++ Forum wohl besser aufgehoben. Denn libstdc++ verhält sich hier nicht (mehr!) Standardkonform, und libc++ genausowenig. Laut Tabelle 101 konstruiert der emplace -Aufruf nämlich das pair<> Objekt per forward . Folgendes ist ein reduziertes Beispiel dass die Diskrepanz aufzeigt:

    std::pair<int, TestX1> p(0, 0);
    

    Der relevante perfect-forwarding Konstruktor soll seit der Aufnahme von N4387 (welche bereits in N4527 vorhanden ist) nicht ins candidate set

    N4527 [pairs.pair]/8 schrieb:

    …unless is_constructible<first_type, U&&>::value is true and is_constructible<second_type, V&&>::value is true .

    Ein entsprechendes static_assert geht sogar völlig ungehindert durch:

    static_assert( std::is_constructible<TestX1, int&&>{} );
    

    is_constructible prüft nämlich direct-initialization, i.e. grob übersetzt, die Gültigkeit des Ausdrucks TestX1(std::move(0)) , was natürlich in Ordnung ist.
    Jedoch verwenden die Konstruktoren von e.g. libc++ nicht is_constructible sondern is_convertible , wie es nämlich wie nach der Aufnahme von N3140 (in N4296 zu finden) korrekt war:

    N4296 [pairs.pair]/9 schrieb:

    Remarks: If U is not implicitly convertible to first_type or V is not implicitly convertible to second_type this constructor shall not participate in overload resolution.

    is_convertible prüft implizite Konvertierungen, wie in [conv]/3 definiert:

    An expression e can be implicitly converted to a type T if and only if the declaration T t=e; is well-formed, for some invented temporary variable t (8.5).

    Allerdings ist hier natürlich ein gravierender Unterschied; copy-initialization von TestX1 via e.g.

    TestX1 t = 0; // Hier 0 weil U/V keine Referenzen sind und wir daher ein prvalue brauchen
    

    impliziert eine temporary:

    [dcl.init]/(17.6.2) schrieb:

    Otherwise (i.e., for the remaining copy-initialization cases), user-defined conversion sequences that can convert from the source type to the destination type […] are enumerated as described in 13.3.1.4, and the best one is chosen through overload resolution (13.3). […] The function selected is called with the initializer expression as its argument; if the function is a constructor, the call initializes a temporary of the cv-unqualified version of the destination type. The temporary is a prvalue. The result of the call (which is the temporary for the constructor case) is then used to direct-initialize […] the object that is the destination of the copy-initialization.

    Wie von meinem Vorposter erwähnt, kannst du daher einen Move-Konstruktor deklarieren, denn schließlich wird die Temporary als rvalue übergeben. Ansonsten musst du wohl warten bis die libstdc++-Entwickler das Paper implementieren.



  • Wow, super, danke für die ausführliche Antwort 🙂


Anmelden zum Antworten