Мне было любопытно, как std::pair
обрабатывает ссылки начиная с C++20, поскольку повторное размещение ссылок, находящихся внутри структур, теперь разрешено с помощью пользовательских функций-членов. См. этот вопрос. У меня есть код, который использует пары с указателями на vector<vector<Stuff>>
, которые я использую для сортировки различных аспектов данных. Мне нравится избавляться от указателей, где это возможно, и использовать ссылки. Это потерпело неудачу, без каких-либо предупреждений или ошибок компилятора, поэтому я написал этот простой код:
#include <tuple>
#include <iostream>
#include <vector>
#include <algorithm>
#include <string>
using std::pair, std::vector, std::string, std::cout;
int main()
{
string s1{ " MyFirst" };
string s2{ " MySecond" };
vector < pair<string, string&>> v; v.reserve(2); // prevent reallocation
v.push_back({ "string_b", s2 });
v.push_back({ "string_a", s1 });
cout << v[0].first << v[0].second << '\n';
cout << v[1].first << v[1].second << '\n';
// sort so string_a comes first
std::sort(v.begin(), v.end(), [](pair<string, string&> a, pair<string, string&> b) {
return a.second < b.second; });
cout << '\n';
cout << v[0].first << v[0].second << '\n';
cout << v[1].first << v[1].second << '\n';
// outputs
/*
string_b MySecond
string_a MyFirst
string_a MySecond
string_b MySecond
*/
Никаких ошибок компилятора, но очень странные результаты, которые не имеют смысла ни до C++20, поскольку он переустанавливает одну из пар, ни после того, как он может дать ожидаемые результаты.
Это было закодировано на MSVC. Похоже, GCC дает те же результаты: обозреватель компилятора
Есть ли какое-либо объяснение тому, что делает std::pair
?
Некоторая дискуссия о том, можно ли повторно разместить ссылки. Раньше такой возможности не было, но C++20 позволяет это в классах, хотя и требует написания специального члена. Вот пример простого класса MyPair со строкой и ссылкой на строку, который работает должным образом и является допустимым кодом.
#include <iostream>
#include <algorithm>
#include <string>
#include <vector>
using std::pair, std::vector, std::string, std::cout;
struct MyPair {
string first;
string& second;
MyPair(const string& s1, string &s2) : first(s1), second(s2) {}
MyPair& operator=(const MyPair& arg) {
if (this == &arg)
return *this;
std::destroy_at(this);
return *std::construct_at(this, arg);
}
};
int main()
{
string s1{ " MyFirst" };
string s2{ " MySecond" };
vector<MyPair> v;
v.push_back({ "string_b", s2 });
v.push_back({ "string_a", s1 });
cout << v[0].first << v[0].second << '\n';
cout << v[1].first << v[1].second << '\n';
// sort so string_a comes first
std::sort(v.begin(), v.end(), [](MyPair &a, MyPair &b) {
return a.second < b.second; });
cout << '\n';
cout << v[0].first << v[0].second << '\n';
cout << v[1].first << v[1].second << '\n';
}
// outputs
/*
string_b MySecond
string_a MyFirst
string_a MyFirst
string_b MySecond
*/
Сортировка копирует или перемещает данные. И ссылки нельзя скопировать или переместить. Думайте о ссылке как о псевдониме чего-то еще: каждый раз, когда вы используете ссылку, вы фактически используете объект, на который она ссылается.
@TedLyngmo Это было верно до C++ 20. Теперь ссылки можно переустанавливать. См. это, которое применимо как к ссылкам, так и к константам.
@Someprogrammerdude. Начиная с С++ 20, они могли быть, но, по-видимому, для пары это не было сделано.
@doug Хорошо, это создает новый объект вместо старого, так что я бы не назвал это переустановкой, но я понял суть. Однако это не ново для C++20. Раньше тоже работало нормально. Пример C++11
@TedLyngmo. До C++20 основные жизненные правила гласили, что вы не можете изменять константы или ссылки после их инициализации при создании. Ситуация радикально изменилась в C++20. Термин «прозрачно заменен». Я надеялся увидеть его в паре. Если это работало в С++ 11, то это был UB.
@doug Хорошо, это увлекательно. Я не рассматриваю это как изменение ссылки, поскольку мы заканчиваем жизнь, вызывая деструктор и создавая новый объект на месте старого объекта - но мне обязательно придется прочитать об этом.
@doug - насколько я помню, тебе всегда разрешалось создавать новый объект вместо ранее уничтоженного. Никогда не было переустановки ссылок ни до, ни после C++20.
@Gene Вы можете освободить хранилище и повторно использовать его, но не можете напрямую ссылаться на его участников. Вам пришлось пройти через std::launder
, что сделало его практически непригодным для использования. C++ 20 устранил необходимость использования std::launder
для этой цели.
@doug — это просто артефакт construct_at
, который был представлен в C++20, то есть return ::new (voidify(*location)) T(std::forward<Args>(args)...);
— он не требует, чтобы location
был указателем на живой объект, в launder
нет необходимости.
@Genestd::launder
требовался до C++20, потому что даже если вы освободили память для объекта и создали на его месте новый, вы не могли ссылаться на константы или ссылки и получать новые значения. По крайней мере технически. Но можно было бы и через отмывку. Начиная с C++20 в этом больше нет необходимости.
Есть ли какое-либо объяснение тому, что делает
std::pair
?
Что касается вопроса выше, то результат следующего фрагмента кода, добавленного в конце вашего примера, скорее всего, вас удивит:
cout << "s1" << s1 << '\n'; // prints "s1 MySecond"
cout << "s2" << s2 << '\n'; // prints "s2 MySecond"
Никакой переустановки ссылок или каких-либо других дополнительных операций не было.
Простой факт заключается в том, что ссылки не являются указателями. Таким образом, копирование/замена данных где-то глубоко внутри сгенерированного кода сортировки вместо замены параметра второй пары, как вы ожидали, генерирует код, перезаписывающий ссылочные строки.
Спасибо. Это действительно то, что происходит. Таким образом, замена двух пар может перезаписать указанные значения. И это объясняет те сумасшедшие вещи, которые я увидел, когда перешел от указателей к ссылкам в своей базе кода. Я не уверен, каково будет использование такого поведения, но очевидно, что именно так работает пара.
std::pair
перемещает/копирует назначение и swap
не переустанавливает ссылочные элементы. Вместо этого они определены для воздействия на объекты, на которые ссылаются, например, если бы вы написали, например. lhs.first = rhs.first
или swap(lhs.first, rhs.first)
для работы с элементами, за исключением того, что категория значений lhs
и rhs
соответствует справочным характеристикам перегрузок.
Таким образом, когда std::sort
пытается поменять местами два члена, он не изменит объекты, на которые ссылаются ссылки, а вместо этого поменяет местами строковые значения, содержащиеся в s1
и s2
.
Как следствие, вы нарушаете семантические требования std::sort
, вызывая неопределенное поведение. Компаратор не сравнивает объекты в диапазоне после замены последовательно.
Интерфейс std::pair
старше, чем C++20, и даже в C++20 подход к «перестановке ссылок» посредством прозрачной замены не во всех случаях применим. В частности, это не сработает, например. если пара является mutable
подобъектом const
полного объекта.
Предлагаемая вами реализация MyPair::operator=
страдает от большего количества крайних случаев, чем необходимо. Например, у него есть UB, если MyPair
используется в качестве базового класса или члена с атрибутом [[no_unique_address]]
.
И в любом случае, пока эта техническая возможность замены ссылок существует, я не думаю, что у кого-то есть намерение сделать эту реальную семантику используемой (или даже поддерживаемой) библиотекой. Ссылка должна быть непереустанавливаемой. Если предполагается переустанавливаемая семантика, следует использовать std::reference_wrapper
. В библиотеке это уже устоялось.
Да, прозрачная замена не работает (это UB) для любых изменяемых элементов константных объектов верхнего уровня или нет. Похоже, у std::swap
будет та же проблема. Определенное поведение для пары, но определенно нарушает ожидания того, что будет делать своп. На самом деле, даже не определено поведение, поскольку порядок не указан.
@doug Это вполне соответствует остальной части стандартной библиотеки и соответствует предполагаемому значению ссылки: любое действие, которое обычно применяется к объекту, вместо этого применяется к объекту, на который ссылается ссылка. А если это не предусмотрено, то следует использовать std::reference_wrapper
. Ссылка ведет себя точно так же, как если бы на ее место был помещен объект, на который она ссылается.
Я действительно попробовал reference_wrapper в своей базе кода. Не удалось заставить его работать. Не знаю почему, ведь я использовал его раньше, но не тратил на это много времени. Думаю, я буду придерживаться подхода с указателем. Просто и понятно.
@doug «На самом деле, даже не определено поведение, поскольку порядок не указан: в самой паре операции четко определены. Проблема заключается только в вашем компараторе для std::sort
, который не последовательно сравнивает элементы диапазона после операций замены. Если бы вместо этого вы сравнили первый элемент, он был бы четко определен.
Что ж, std::sort
раньше никогда не приходилось задумываться о таких вещах. Я склонен думать о константных и ссылочных членах как об инвариантах конкретного объекта. Я думаю о присвоении объекта как о замене всего объекта новым, подчиняющимся тем же инвариантам, что и тип.
Я действительно попробовал reference_wrapper в своей базе кода. Не удалось заставить его работать. Похоже, ты делаешь что-то не так. Все, что вам нужно сделать в вашем примере, это заменить string&
в аргументе пары на std::reference_wrapper<string>
и добавить .get()
или static_cast<std::string>()
при доступе к обернутому объекту для печати, вам даже не нужно изменять лямбда обратного вызова, поскольку преобразование будет выполнено неявно.
В моем примере работает нормально. В моей кодовой базе это просто не сработало: vector<vector<Stuff>>
Понятия не имею, почему. Слишком сложно упростить, чтобы опубликовать здесь.
По поводу переустановки ссылок: мне кажется, вы что-то не так поняли. См. isocpp.org/wiki/faq/references#reseating-refs