Podczas wywoływania std::map::emplace z argumentami takimi jak map.emplace(key, value_args...), standard C++ wymaga od implementacji skonstruowania tymczasowego std::pair<const Key, T> (lub jego odpowiednika węzła) przed sprawdzeniem unikalności klucza. Jeśli klucz już istnieje, ten węzeł jest natychmiast odrzucany, co oznacza, że wszelkie kosztowne konstrukcje wartości skojarzonej T były stratą.
Znacznik std::piecewise_construct modyfikuje to zachowanie, sygnalizując kontenerowi, aby traktował następujące dwa argumenty krotki jako listy argumentów dla konstruktorów klucza i wartości. Owijając argumenty konstruktora w std::forward_as_tuple, kontener opóźnia faktyczną instancję wartości skojarzonej do momentu wewnątrz nowo przydzielonego węzła, i tylko jeśli klucz zostanie potwierdzony jako unikalny. Zapewnia to, że wartość jest konstruowana dokładnie raz, w ostatecznej lokalizacji w pamięci, i nigdy, jeśli wstawienie się nie powiedzie.
W platformie handlu wysokiej częstotliwości musieliśmy przechowywać w pamięci zdeserializowane obiekty Order (ciężkie struktury zawierające wektory i łańcuchy) w std::map<OrderID, Order>. Początkowa implementacja używała orders.emplace(id, DeserializeOrder(buffer)). Profilowanie ujawniło, że podczas szczytów rynkowych 15% czasu CPU zostało zmarnowanych na konstrukcję obiektów Order dla duplikatów ID, które były natychmiast odrzucane przez logikę odrzucania mapy.
Rozwiązanie 1: Sprawdź, a następnie wstaw. Rozważaliśmy jawne sprawdzenie if (orders.find(id) == orders.end()) przed wywołaniem emplace. To unikało marnotrawienia konstrukcji, ale wymagało dwóch Traversals drzewa—jednej dla find i innej dla emplace—podwajając koszt porównania i szkodząc lokalizacji w pamięci podręcznej.
Rozwiązanie 2: Ręczne wydobycie węzła. Zbadaliśmy możliwość utworzenia ręcznie std::map::node_type za pomocą orders.extract(id) i ponownego wstawienia, jeśli był pusty, ale to wymagało wcześniejszej konstrukcji Order poza mapą do wypełnienia węzła, ponownie wprowadzając pierwotny problem.
Rozwiązanie 3: std::piecewise_construct. Zastosowaliśmy orders.emplace(std::piecewise_construct, std::forward_as_tuple(id), std::forward_as_tuple(buffer)). To opóźniło deserializację do momentu, gdy węzeł miał być wstawiony. Chociaż rozwiązało to problem wydajności, składnia była obszerna i podatna na błędy dotyczące czasu życia argumentów.
Wybrane podejście i wynik: Ostatecznie przeszliśmy na C++17 i użyliśmy orders.try_emplace(id, buffer). To zapewniło tę samą gwarancję wydajności—budując Order tylko przy udanej wstawce—z czystszą składnią i mniejszym ryzykiem dangling references. Opóźnienie systemu spadło o 12% podczas szczytowego obciążenia.
Dlaczego należy używać std::forward_as_tuple zamiast std::make_tuple przy przygotowywaniu argumentów dla std::piecewise_construct?
std::make_tuple tworzy krotkę, rozkładając swoje argumenty; kopiuje lub przenosi wartości do pamięci krotki. Jeśli typ skojarzony jest niekopiowalny lub jeśli przekazujesz duże obiekty, make_tuple albo nie kompiluje się, albo wiąże się z niepotrzebnym kosztem kopii. std::forward_as_tuple tworzy krotkę odniesień (lvalue lub rvalue), które zachowują oryginalną kategorię wartości, umożliwiając doskonałe przekazywanie bezpośrednio do konstruktora obiektu bez pośrednich kopii.
Kiedy używasz std::piecewise_construct, dlaczego istotne jest, aby upewnić się, że odniesienia owinięte w std::forward_as_tuple pozostają ważne aż do zakończenia wstawiania?
forward_as_tuple nie wydłuża czasu życia tymczasowych argumentów, które do niego przekazano; jedynie przechwytuje odniesienia. Jeśli napiszesz map.emplace(std::piecewise_construct, std::forward_as_tuple(CreateTempKey()), std::forward_as_tuple(args...)), tymczasowy obiekt zwrócony przez CreateTempKey() jest niszczony na końcu pełnego wyrażenia, zanim emplace wewnętrznie spróbuje skonstruować węzeł. To pozostawia krotkę z wiszącym odniesieniem, co skutkuje niezdefiniowanym zachowaniem, gdy konstruktor uzyskuje dostęp do klucza.
Jak std::map::try_emplace różni się od idiomu emplace + piecewise_construct w odniesieniu do obsługi samego klucza?
Podczas gdy piecewise_construct może opóźnić konstrukcję zarówno klucza, jak i wartości, try_emplace wyraźnie oddziela klucz od argumentów konstrukcyjnych wartości. try_emplace przyjmuje klucz przez referencję (lub wartość) i tylko przekazuje pozostałe argumenty do konstruktora typu skojarzonego, jeśli wstawienie się powiedzie. Oznacza to, że try_emplace nie może skonstruować klucza na miejscu z wielu argumentów—wymaga, aby obiekt klucza już istniał lub był konstruowalny z jednego argumentu—gdzie piecewise_construct może opóźnić konstrukcję obu komponentów. Jednak try_emplace eliminuje syntaktyczną obszerność i zagrożenia związane z czasem życia ręcznego zarządzania krotką.