std::pair & std::make_pair



  • Hallo zusammen,

    eine Interessensfrage. Ich habe mich erst letztlich gefragt, warum ich eigentlich immer std::make_pair schreibe anstelle von std::pair.

    Ich habe dazu auch bereits diverse Gründe gefunden. Nach meinem jetzigen Verständnis hat man dieses Konstrukt, dass der Compiler aus einer Template-Funktion (std::make_pair) implizit den korrekten Datentyp wählt oder castet.

    std::pair <int,int> foo;
      std::pair <int,int> bar;
    
      foo = std::make_pair (10,20);
      bar = std::make_pair (10.5,'A'); // ok: implicit conversion from pair<double,char>
    

    Q: http://www.cplusplus.com/reference/utility/make_pair/

    Warum kann, dass der Compiler nicht bei einem Klassentemplate, wenn dieses ein R-Value ist? Warum ist auch an dieser Stelle eine explizite Definition notwendig? (Es sollte sich doch aus dem L-Value ableiten lassen, oder nicht?)

    std::pair <int,int> foo;
      std::pair <int,int> bar;
    
      foo = std::pair (10,20);
      bar = std::pair (10.5,'A');
    

    Vielen Dank fürs Aufschlauen.

    Einen schönen Abend wünsche ich.

    Viele Grüße



  • Ich glaube das hat einfach den Grund, dass man die Komplexität der Sprache im Zaun halten wollte. Der Konstruktor ist erst durch die Klasse definiert, welche sich implizit aus den Konstruktorargumenten ergibt. Das ist wahrscheinlich schwer aufzulösen und eher selten nötig, weswegen man sich für den Umweg über make_xyz entschieden hat. Aber das ist eher ein wenig Spekulation meinerseits. Mich würde da eine Antwort von jemandem, der sich da besser auskennt auch sehr interessieren.



  • goi schrieb:

    Ich glaube das hat einfach den Grund, dass man die Komplexität der Sprache im Zaun halten wollte.

    Sorry für Offtopic, aber man "hält etwas im Zaum."



  • jb schrieb:

    Warum kann, dass der Compiler nicht bei einem Klassentemplate, wenn dieses ein R-Value ist? Warum ist auch an dieser Stelle eine explizite Definition notwendig? (Es sollte sich doch aus dem L-Value ableiten lassen, oder nicht?)

    std::pair <int,int> foo;
      std::pair <int,int> bar;
    
      foo = std::pair (10,20);
      bar = std::pair (10.5,'A');
    

    Das hat sicher auch damit zu tun, dass man Templates spezialisieren kann und es damit keine 1:1 Beziehung zwischen Templateparametern und Typen von Konstruktorparameter geben muss, eben auch dann nicht, wenn es beim Basis-Template so aussieht. Die Template-Spezialisierung in C++ ohne irgendwelche Nebenbedingungen, die man für Typ-Deduktion ausnutzen könnte, hat eben auch Nachteile. Da kann man bestimmt irgendwie word-arounds für basteln, die die Sprache noch komplexer machen würden. Die Frage ist nur: Lohnt sich das? Vielleicht gab's in der Richtung auch schon einen Vorschlag. Wenn ja, ist er aber nicht weit gekommen. Ich kann mich zumindest nicht dran erinnern und bin eigentlich halbwegs auf dem Laufenden, was da so grob passiert in Sachen Standardisierung.

    In Sprachen, wo es keine C++ ähnlichen Template-Spezialisierungen gibt, geht das. Da kann man sich dann auch eine etwas mächtigere Typ-Deduktion à la Hindley-Milner leisten. In Rust kann ich z.B. folgendes schreiben:

    struct Pair<X, Y>(X, Y);
    
    fn main() {
        let p = Pair(42, 3.14159265); // p ist ein Pair<i32,f64>
    }
    

    Allerdings braucht man in Rust auch kein generisches `Pair`. Die Sprache bietet Tupel von Haus aus.


  • Mod

    Wenn ja, ist er aber nicht weit gekommen.

    Ich fürchte, doch: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4471.html

    Die Sprache bietet Tupel von Haus aus.

    Und was hast du jetzt bewiesen?



  • Geht doch:

    std::pair <int,int> foo;
    std::pair <int,int> bar;
    
    foo = {10,20};
    bar = {10.5,'A'};
    


  • Arcoth schrieb:

    Wenn ja, ist er aber nicht weit gekommen.

    Ich fürchte, doch: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2015/n4471.html

    Auf Dich kann man sich verlassen, den richtigen Link rauszusuchen. 🙂

    Immerhin schon Revision 2. Ich glaube, Revision 1 hatte ich damals auch wahrgenommen und dachte mir "Cool!". Dass da zwei Jahre später nochmal was kam, ist mir entgangen; deswegen kam das "nicht weiter gekommen" von mir.

    Und? Wie hat der Autor das Problem mit möglichen Spezialisierungen gelöst? Ganz einfach: Man ignoriert es.

    Note that after the deduction process described above the initialization may still end up being ill-formed. For example, a selected constructor might be inaccessible or deleted, or the selected template instance might have been specialized or partially specialized in such a way that the candidate constructors will not match the initializer.

    Gut. Muss ja auch nicht immer funktionieren. Ist halt eh alles Compile-Time Duck-Typing, was C++ da (noch) zu bieten hat. Da kann auch schonmal was schiefgehen.

    Spannend ist auch die Frage, wie die Interaktion mit Rvalue-Referenzen aussieht. Na? Sieht da schon jemand ein Problem? C++ wär ja nicht C++, wenn es nicht komische Interaktionen zwischen Sprachmerkmalen gäbe, die die Designer nicht bedacht haben. Ich hab' dem Autor mal ne liebe Mail geschickt und drauf hingewiesen.

    Arcoth schrieb:

    Die Sprache bietet Tupel von Haus aus.

    Und was hast du jetzt bewiesen?

    Nix. Das war eine kostenlose Zusatzinformation der Sorte nice-to-know.


  • Mod

    Gut. Muss ja auch nicht immer funktionieren.

    Ganz genau! Partielle Spezialisierung ist ein Feature, Deduzierung ein anderes. Wenn Du letzteres möchtest, lässt du ersteres weg - oder stellst sicher, dass die Konstruktoren kompatibel sind. Es gibt kein Problem.

    Dass C++ Features in einer festen Reihenfolge fabriziert werden, und daher Inkompabilität zwischen bestimmten Features entsteht, ist kein Nachteil gegenüber Sprachen, in denen Features tatsächlich alle in einen Topf geschmissen werden können. Man muss sich eben der Nuancen bewusst sein. Ob Rust den Vorteil der Simplizität nicht zu einem gewissen Preis anbietet, ist nicht bekannt, oder? Warum der blinde Eifer?

    Ist halt eh alles Compile-Time Duck-Typing, was C++ da (noch) zu bieten hat.

    Mehr oder weniger. Wo es nicht mehr funktioniert, setzen wir Concepts (oder manuelles SFINAE) ein.

    Da kann auch schon mal was schiefgehen.

    Es geht nichts schief, was nicht schiefgehen sollte. Diese Form von Deduzierung funktioniert wofür sie konzipiert ist. Wenn man nicht verstanden hat, wie der Prozess funktioniert, kann und wird etwas "schiefgehen", ja. Andernfalls wird man entweder auf den Luxus verzichten, oder das Problem, dass die Spezialisierung löste, anders umgehen. Letztendlich können wir alles, was wir können sollten und wollen: Was will man mehr?

    Spannend ist auch die Frage, wie die Interaktion mit Rvalue-Referenzen aussieht.

    Ich verstehe nicht, worauf Du Dich beziehst. Forwarding references werden weiterhin deduziert wie gewohnt.

    C++ wär ja nicht C++, wenn es nicht komische Interaktionen zwischen Sprachmerkmalen gäbe, die die Designer nicht bedacht haben.

    Ich stimme zu. Das macht das Studium und die Erweiterung der Sprache interessant. 🙂

    Ich hab' dem Autor mal ne liebe Mail geschickt und drauf hingewiesen.

    R2 ist praktisch ein abstraktes Dokument, das für abstraktes Feedback gedacht ist. Wenn's Dich glücklich macht, auf (mögliche) Petitessen hinzuweisen, während nicht einmal wording vorhanden ist... 😕

    Ich kann mich zumindest nicht dran erinnern und bin eigentlich halbwegs auf dem Laufenden, was da so grob passiert in Sachen Standardisierung.

    Darf ich das auf die Probe stellen: Kannst Du erklären, in welchem (C++17) Kontext C{...Ts} void f(); gültig ist? Vorausgesetzt C ist entsprechend deklariert worden.

    Nix. Das war eine kostenlose Zusatzinformation der Sorte nice-to-know.

    Ganz genau. Dass Rust Probleme löst, ohne welche zu erzeugen - d.h. eine strikte Verbesserung ist, hast Du nicht bewiesen. Trotzdem reibst Du uns diese Sprache immer wieder süffisant ein, als ob sie der heilige Grahl wäre. Damit wird nicht der Popularität der Sprache gedient. Du erzeugst stattdessen eine Aversion gegen die sie nutzenden Ignorami. const wird für Dokumentation verwendet, und spielt in Optimierungen keine Rolle? Was kommt als nächstes, C++ ist ein marginale Obermenge von C, dessen Probleme Rust ja löst?

    ➡ Ich werde auf Volkards Gutachten warten.



  • Warum der blinde Eifer?

    Das gleitet mir jetzt zuweit ab hier.

    Arcoth schrieb:

    Spannend ist auch die Frage, wie die Interaktion mit Rvalue-Referenzen aussieht.

    Ich verstehe nicht, worauf Du Dich beziehst. Forwarding references werden weiterhin deduziert wie gewohnt.

    Der Knackpunkt ist, dass über die beschriebenden Regeln sich der Kontext von Templateparametern verschiebt. Beispiel:

    template<class T>
    struct Wrapper {
        T value;
    
        Wrapper(T const& x): value(x) {}
        Wrapper(T && y): value(std::move(y)) {}
    };
    

    Hier ist T zwar ein Templateparameter, aber nicht im Sinne eines Funktionstemplates. T wird festgelegt, indem man sich für eine Spezialisierung entscheidet. Wenn ich dann schreibe:

    int main() {
        std::string s = "123";
        Wrapper<std::string> blah = s;
    }
    

    steht T schon fest, bevor hier überhaupt nach gültigen Konstruktor-Kandidaten gesucht wird. Im Besonderen ist y keine universelle Referenz. Schreibe ich aber stattdessen

    int main() {
        std::string s = "123";
        auto w = Wrapper(s);
    }
    

    was bekomme ich dann für einen Wrapper? Kompiliert das überhaupt? Nein, es kompiliert erst gar nicht -- auch nicht mit den vorgeschlagenen Regeln aus dem Proposal. Warum? Weil hier auf einmal dank der Perfect-Forwarding-Regel T mit std::string& deduziert wird. Die Move-Variante ist hier auf einmal der bessere Kandidat, weil er keine const-Konvertierung nötig hat und damit spezieller ist. Der Compiler versucht eine Spezialisierung von Wrapper<std:string&> zu bauen und stellt dann fest, dass man eine Lvalue-Referenz ( Wrapper<std:string&>::value ) ja doch nicht mit einem Xvalue-Ausdruck vom Typ std::string initialisieren kann. Oops.

    Und jetzt? Man könnte eine neue Sonderregel konstruieren, die in solchen Fällen, diese perfect-forwarding-Regel aushebelt, damit in diesem Fall das passiert, was jeder Anfänger erwartet hätte. Oder man könnte die Regeln so lassen und sagen: "Ja, wenn du Type-Deduction beim Konstruktor haben willst, dann darfst Du da dieses Überladungsmuster für Move-Semantik nicht mehr benutzen". Beides ist irgendwie...doof.

    Arcoth schrieb:

    Ich kann mich zumindest nicht dran erinnern und bin eigentlich halbwegs auf dem Laufenden, was da so grob passiert in Sachen Standardisierung.

    Darf ich das auf die Probe stellen: Kannst Du erklären, in welchem (C++17) Kontext C{...Ts} void f(); gültig ist?

    Das ist 'ne Idee (AFAIK mit wenig Rückendeckung), wie eine alternative Funktions-Template-Deklaration aussehen könnte, wobei C ein Concept ist und Ts als Template-Parameter-Pack eingeführt wird. Ich weiß aber auch schon nicht mehr, woher ich das jetzt weiß. Ich hab's wahrscheinlich irgendwie über den /r/cpp Subreddit mitbekommen, vielleicht aus einem der Trip-Reports von letzten Treffen des S-Komitees.


  • Mod

    Und jetzt?

    Hast du die zweite Revision auch gelesen?

    template<class T>
    struct Wrapper {
        T value;
        template <typename U>
        Wrapper<std::remove_reference_t<U>>(U && u): value(std::forward<U>(u)) {}
    };
    

    Muss fast nie gemacht werden, da es sehr selten auftritt - und die gezeigte Lösung ist noch prägnant. Mir fällt auf anhieb nur optional ein, wo das nötig werden wird.

    Und bei einem Feature, das zum absoluten Großteil nur in Bibliotheken (korrekt) implementiert werden muss, auf Anfänger Verwirrtheit zu plädieren ist ziemlich... sinnlos.

    Das ist 'ne Idee (AFAIK mit wenig Rückendeckung), wie eine alternative Funktions-Template-Deklaration aussehen könnte, wobei C ein Concept ist und Ts als Template-Parameter-Pack eingeführt wird. Ich weiß aber auch schon nicht mehr, woher ich das jetzt weiß. Ich hab's wahrscheinlich irgendwie über den /r/cpp Subreddit mitbekommen, vielleicht aus einem der Trip-Reports von letzten Treffen des S-Komitees.

    👍 Richtig. Siehe Concepts TS §14.2. Fand ich so abstrus dass ich es teilen musste 🙂



  • Arcoth schrieb:

    Und jetzt?

    Hast du die zweite Revision auch gelesen?

    template<class T>
    struct Wrapper {
        T value;
        template <typename U>
        Wrapper<std::remove_reference_t<U>>(U && u): value(std::forward<U>(u)) {}
    };
    

    Muss fast nie gemacht werden, da es sehr selten auftritt - und die gezeigte Lösung ist noch prägnant. Mir fällt auf anhieb nur optional ein, wo das nötig werden wird.

    Auweia. Nee, den letzten Absatz hatte ich nicht gelesen. Das erfordert natürlich als Klassenautor etwas mehr Weitsicht. Man sollte hier das U wahrscheinlich auch noch einschränken.

    Arcoth schrieb:

    Und bei einem Feature, das zum absoluten Großteil nur in Bibliotheken (korrekt) implementiert werden muss, auf Anfänger Verwirrtheit zu plädieren ist ziemlich... sinnlos.

    Ich weiß nicht. Das überzeugt mich nicht so ganz. Gut, die, die noch nichts von Rvalue-Referenzen gehört haben, werden natürlich damit auch nicht auf die Klappe fliegen, aber das heißt nicht, dass dieser Fallstrick kostenfrei ist.

    Arcoth schrieb:

    Das ist 'ne Idee (AFAIK mit wenig Rückendeckung), wie eine alternative Funktions-Template-Deklaration aussehen könnte, wobei C ein Concept ist und Ts als Template-Parameter-Pack eingeführt wird. Ich weiß aber auch schon nicht mehr, woher ich das jetzt weiß. Ich hab's wahrscheinlich irgendwie über den /r/cpp Subreddit mitbekommen, vielleicht aus einem der Trip-Reports von letzten Treffen des S-Komitees.

    👍 Richtig. Siehe Concepts TS §14.2. Fand ich so abstrus dass ich es teilen musste 🙂

    Ok, dann kann ich jetzt nicht ausschließen, dass ich das von Dir weiß. 🙂

    Arcoth schrieb:

    Dass Rust Probleme löst, ohne welche zu erzeugen - d.h. eine strikte Verbesserung ist, hast Du nicht bewiesen.

    Kann mich nicht dran erinnern, das behauptet zu haben.

    Arcoth schrieb:

    Was kommt als nächstes, C++ ist ein marginale Obermenge von C...

    Und ich dachte, Du würdest mich besser kennen.


  • Mod

    krümelkacker schrieb:

    Und ich dachte, Du würdest mich besser kennen.

    Ich kenne dich nicht wirklich, nichtsdestotrotz muss ich mich für meine Barschheit entschuldigen. Ich habe halt meinen C++-Stolz, der kommt immer wieder raus. Sorry. 🙂

    Gut, die, die noch nichts von Rvalue-Referenzen gehört haben, werden natürlich damit auch nicht auf die Klappe fliegen, aber das heißt nicht, dass dieser Fallstrick kostenfrei ist.

    Seh ich auch so. C++ leidet merklich unter dem immensen Haufen von Features. Wer sich darin zurechtfindet, fühlt sich herrlich mächtig. Der Großteil ist das nicht.


  • Mod

    krümelkacker schrieb:

    Man sollte hier das U wahrscheinlich auch noch einschränken.

    Nein, wie auch?



  • interessant, ich frag mich jedoch
    ist das

    Arcoth schrieb:

    template<class T>
    struct Wrapper {
        T value;
        template <typename U>
        Wrapper<std::remove_reference_t<U>>(U && u): value(std::forward<U>(u)) {}
    };
    

    gleichwertig oder besser als

    krümelkacker schrieb:

    template<class T>
    struct Wrapper {
        T value;
    
        Wrapper(T const& x): value(x) {}
        Wrapper(T && y): value(std::move(y)) {}
    };
    

    und wenn ja warum brauch ich im ersteren kein std::move mehr?


  • Mod

    Es ist nicht ganz äquivalent; wenn in der ersten Variante ein non-const lvalue übergeben wird, wird ein const lvalue weitergereicht. Dieses Problem hat meine Variante nicht. Und das move wird weggelassen, da das Argument eben nicht zwangsläufig ein rvalue war - forward verhält sich in dem Fall wie move , andernfalls gibt es ein lvalue.



  • Hallo zusammen,

    sry. für meine späte Antwort.

    Also heißt es, dass durch die automatische Ableitung ein falscher Konstruktor aufgerufen werden könnte - oder?

    ABER: Warum funktioniert es dann bei einer Funktion?

    Vielen Dank.



  • kurze_frage schrieb:

    ist das

    Arcoth schrieb:

    template<class T>
    struct Wrapper {
        T value;
        template <typename U>
        Wrapper<std::remove_reference_t<U>>(U && u): value(std::forward<U>(u)) {}
    };
    

    gleichwertig oder besser als

    krümelkacker schrieb:

    template<class T>
    struct Wrapper {
        T value;
    
        Wrapper(T const& x): value(x) {}
        Wrapper(T && y): value(std::move(y)) {}
    };
    

    und wenn ja warum brauch ich im ersteren kein std::move mehr?

    Das erste ist ja aktuell nicht gültig und nur eine Lösung für ein Problem, was im zweiten Fall in Kombination mit den vorgeschlagenen Deduktionsregeln auftritt.

    Das std::move wird hier effektiv durch perfect forwarding ersetzt, was eben beim Weitergegen von u auch wieder die richtige Wertkategorie herstellt (inklusive Rvalueness).

    Ich hatte ja dem Autor des Proposals dieses Beispiel geschickt und damit eine Diskussion zwischen ihm und zwei anderen Komitee-Mitgliedern losgetreten. Ich stand da immer mit im cc und habe bis jetzt 9 Mails bekommen. Die erste Antwort, die ich bekam, sagte im Wesentlichen, dass in der Situation ein "explicit deduction guide" nötig ist. Das könnte z.B. so aussehen:

    template<class T>
    struct Wrapper {
        T value;
    
        Wrapper(T const& x)
        : value(x) {}
    
        Wrapper(T && x) -> Wrapper<std::decay_t<T>>
        //              ^^^^^^^^^^^^^^^^^^^^^^^^^^^
        //                   "deduction guide"
        : value(std::forward<T>(x)) {}
    };
    

    Die Frage ist jetzt noch, ob solche "deduction guides" immer explizit sein sollen, damit die Deduktion für T beim Konstruktor funktioniert, ob die impliziten "deduction guides" angepasst werden sollen, oder ob das Proposal so bleiben soll, wie es ist. Da herrscht anscheinend gerade keine Einigkeit.


Log in to reply