it-swarm.com.de

Warum gibt es in C++ 17 kein std :: construct_at?

C++ 17 fügt std::destroy_at hinzu, aber es gibt kein std::construct_at-Gegenstück. Warum das? Könnte es nicht so einfach wie folgt implementiert werden?

template <typename T, typename... Args>
T* construct_at(void* addr, Args&&... args) {
  return new (addr) T(std::forward<Args>(args)...);
}

Was würde es ermöglichen, diese nicht ganz natürliche Platzierung neu Syntax zu vermeiden:

auto ptr = construct_at<int>(buf, 1);  // instead of 'auto ptr = new (buf) int(1);'
std::cout << *ptr;
std::destroy_at(ptr);
55
Daniel Langr

std::destroy_at bietet zwei objektive Verbesserungen gegenüber einem direkten Destruktoraufruf:

  1. Es reduziert die Redundanz:

    T *ptr = new T;
    //Insert 1000 lines of code here.
    ptr->~T(); //What type was that again?
    

    Sicher, wir würden es alle lieber vorbereiten, es in einen unique_ptr zu packen und damit fertig zu sein, aber wenn dies aus irgendeinem Grund nicht passieren kann, ist T ein Element der Redundanz. Wenn wir den Typ in U ändern, müssen wir jetzt den Destruktor-Aufruf ändern oder Dinge brechen. Durch die Verwendung von std::destroy_at(ptr) müssen Sie dasselbe an zwei Stellen ändern.

    TROCKEN ist gut.

  2. Das macht es einfach:

    auto ptr = allocates_an_object(...);
    //Insert code here
    ptr->~???; //What type is that again?
    

    Wenn wir den Typ des Zeigers hergeleitet haben, wird das Löschen schwer. Sie können ptr->~decltype(ptr)() nicht tun; da der C++ - Parser nicht so funktioniert. Nicht nur das, decltype leitet den Typ als pointer ab, daher müssten Sie eine Zeiger-Umleitung aus dem abgeleiteten Typ entfernen. Sie führen zu:

    auto ptr = allocates_an_object(...);
    //Insert code here
    using delete_type = std::remove_pointer_t<decltype(ptr)>;
    ptr->~delete_type();
    

    Und wer möchte das eingeben?

Im Gegensatz dazu bietet Ihr hypothetischer std::construct_at keine Ziel Verbesserungen gegenüber der Platzierung new. In beiden Fällen müssen Sie den Typ angeben, den Sie erstellen. In beiden Fällen müssen die Parameter für den Konstruktor angegeben werden. Der Zeiger auf den Speicher muss in beiden Fällen bereitgestellt werden.

Es besteht also keine Notwendigkeit, durch Ihren hypothetischen std::construct_at gelöst zu werden.

Und es ist objektiv weniger fähig als neu platziert. Du kannst das:

auto ptr1 = new(mem1) T;
auto ptr2 = new(mem2) T{};

Dies sind anders . Im ersten Fall wird das Objekt standardmäßig initialisiert, wodurch es möglicherweise nicht initialisiert wird. Im zweiten Fall wird das Objekt wertinitialisiert.

Ihr hypothetischer std::construct_at kann Ihnen nicht erlauben, den gewünschten auszuwählen. Es kann Code enthalten, der eine Standardinitialisierung durchführt, wenn Sie keine Parameter angeben. Es kann dann jedoch keine Version für die Wertinitialisierung bereitgestellt werden. Und es könnte Wert ohne Parameter initialisieren, aber dann konnte das Objekt nicht standardmäßig initialisiert werden.

40
Nicol Bolas

Es gibt so etwas, aber nicht so benannt, wie man es erwarten könnte :

  • uninitialized_copy kopiert einen Bereich von Objekten in einen nicht initialisierten Speicherbereich

  • uninitialized_copy_n (C++ 11) kopiert mehrere Objekte in einen nicht initialisierten Speicherbereich (Funktionsvorlage)

  • uninitialized_fill kopiert ein Objekt in einen nicht initialisierten Speicherbereich, der durch einen Bereich definiert ist (Funktionsvorlage)

  • uninitialized_fill_n kopiert ein Objekt in einen nicht initialisierten Speicherbereich, der durch einen Start und einen Count definiert wird (Funktionsvorlage)
  • uninitialized_move (C++ 17) verschiebt einen Bereich von Objekten in einen nicht initialisierten Speicherbereich (Funktionsvorlage)
  • uninitialized_move_n (C++ 17) verschiebt eine Anzahl von Objekten in einen nicht initialisierten Speicherbereich (Funktionsvorlage)
  • uninitialized_default_construct (C++ 17) erstellt Objekte durch Standardinitialisierung in einem nicht initialisierten Speicherbereich, der durch einen Bereich definiert ist (Funktionsvorlage)
  • uninitialized_default_construct_n (C++ 17) erstellt Objekte durch Standardinitialisierung in einem nicht initialisierten Speicherbereich, der durch einen Start und eine Zählung definiert wird (Funktionsvorlage)
  • uninitialized_value_construct (C++ 17) erstellt Objekte durch Wertinitialisierung in einem nicht initialisierten Speicherbereich, der durch einen Bereich definiert ist (Funktionsvorlage)
  • uninitialized_value_construct_n (C++ 17) erstellt Objekte durch Wertinitialisierung in einem nicht initialisierten Speicherbereich, der durch einen Start und eine Zählung definiert wird 
13
Marek R

Es gibt std::allocator_traits::construct . Früher gab es noch einen weiteren in std::allocator, aber das wurde entfernt, die Begründung in Standards Committee Paper D0174R0 .

8
jakub_d

std::construct_at wurde zu C++ 20 hinzugefügt. Das Papier, das dies tat, ist Mehr constexpr Behälter . Vermutlich hat dies nicht genügend Vorteile gegenüber der Platzierung in C++ 17, aber C++ 20 ändert die Dinge.

Der Zweck des Vorschlags, der diese Funktion hinzugefügt hat, besteht darin, die Zuweisung von constexpr-Speicher, einschließlich std::vector, zu unterstützen. Dies erfordert die Fähigkeit, Objekte in zugewiesenen Speicher zu konstruieren. Es handelt sich jedoch nur um einfache Platzierung neuer Angebote in Bezug auf void *, nicht auf T *. Die constexpr-Auswertung kann derzeit nicht auf den Raw-Speicher zugreifen, und das Komitee möchte, dass dies auch so bleibt. Die Bibliotheksfunktion std::construct_at fügt eine typisierte Schnittstelle constexpr T * construct_at(T *, Args && ...) hinzu.

Dies hat auch den Vorteil, dass der Benutzer den zu erstellenden Typ nicht angeben muss. Es wird vom Typ des Zeigers abgeleitet. Die Syntax zum korrekten Aufrufen der neuen Platzierung ist entsetzlich und kontraintuitiv. Vergleichen Sie std::construct_at(ptr, args...) mit ::new(static_cast<void *>(ptr)) std::decay_t<decltype(*ptr)>(args...).

4
David Stone

Ich denke, es sollte eine Standard-Konstruktfunktion geben. Tatsächlich hat libc ++ ein Implementierungsdetail in der Datei stl_construct.h.

namespace std{
...
  template<typename _T1, typename... _Args>
    inline void
    _Construct(_T1* __p, _Args&&... __args)
    { ::new(static_cast<void*>(__p)) _T1(std::forward<_Args>(__args)...); }
...
}

Ich denke, es ist etwas Nützliches zu haben, weil es die "Platzierung" eines Freundes ermöglicht. Dies ist ein hervorragender Anpassungspunkt für einen Nur-Verschiebetyp, der uninitialized_copy in den Standardheapspeicher benötigt (z. B. von einem std::initializer_list-Element).


Ich habe eine eigene Container-Bibliothek, die einen detail::uninitialized_copy (eines Bereichs) reimplementiert, um einen benutzerdefinierten detail::construct zu verwenden:

namespace detail{
    template<typename T, typename... As>
    inline void construct(T* p, As&&... as){
        ::new(static_cast<void*>(p)) T(std::forward<As>(as)...);
    }
}

Welches ist ein Freund einer Nur-Umzugs-Klasse, um das Kopieren nur im Zusammenhang mit der Platzierung neu zuzulassen.

template<class T>
class my_move_only_class{
    my_move_only_class(my_move_only_class const&) = default;
    friend template<class TT, class...As> friend void detail::construct(TT*, As&&...);
public:
    my_move_only_class(my_move_only_class&&) = default;
    ...
};
0
alfC

construct scheint keinen syntaktischen Zucker bereitzustellen. Darüber hinaus ist es weniger effizient als eine Neuplatzierung. Die Bindung an Referenzargumente führt zu temporärer Materialisierung und zusätzlicher Konstruktion für das Verschieben/Kopieren:

struct heavy{
   unsigned char[4096];
   heavy(const heavy&);
};
heavy make_heavy(); // Return a pr-value
auto loc = ::operator new(sizeof(heavy));
// Equivalently: unsigned char loc[sizeof(heavy)];

auto p = construct<heavy>(loc,make_heavy()); // The pr-value returned by
         // make_heavy is bound to the second argument,
         // and then this arugment is copied in the body of construct.

auto p2 = new(loc) auto(make_heavy()); // Heavy is directly constructed at loc
       //... and this is simpler to write!

Leider gibt es keine Möglichkeit, diese zusätzlichen Konstruktionen beim Kopieren/Verschieben beim Aufruf einer Funktion zu vermeiden. Die Weiterleitung ist fast perfekt.

Andererseits könnte construct_at in der Bibliothek das Standardvokabular der Bibliothek vervollständigen.

0
Oliv