Странное поведение std::pair, содержащего ссылки

Мне было любопытно, как 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
*/

По поводу переустановки ссылок: мне кажется, вы что-то не так поняли. См. isocpp.org/wiki/faq/references#reseating-refs

Ted Lyngmo 01.09.2024 22:03

Сортировка копирует или перемещает данные. И ссылки нельзя скопировать или переместить. Думайте о ссылке как о псевдониме чего-то еще: каждый раз, когда вы используете ссылку, вы фактически используете объект, на который она ссылается.

Some programmer dude 01.09.2024 22:03

@TedLyngmo Это было верно до C++ 20. Теперь ссылки можно переустанавливать. См. это, которое применимо как к ссылкам, так и к константам.

doug 01.09.2024 22:10

@Someprogrammerdude. Начиная с С++ 20, они могли быть, но, по-видимому, для пары это не было сделано.

doug 01.09.2024 22:12

@doug Хорошо, это создает новый объект вместо старого, так что я бы не назвал это переустановкой, но я понял суть. Однако это не ново для C++20. Раньше тоже работало нормально. Пример C++11

Ted Lyngmo 01.09.2024 22:19

@TedLyngmo. До C++20 основные жизненные правила гласили, что вы не можете изменять константы или ссылки после их инициализации при создании. Ситуация радикально изменилась в C++20. Термин «прозрачно заменен». Я надеялся увидеть его в паре. Если это работало в С++ 11, то это был UB.

doug 01.09.2024 22:30

@doug Хорошо, это увлекательно. Я не рассматриваю это как изменение ссылки, поскольку мы заканчиваем жизнь, вызывая деструктор и создавая новый объект на месте старого объекта - но мне обязательно придется прочитать об этом.

Ted Lyngmo 01.09.2024 22:37

@doug - насколько я помню, тебе всегда разрешалось создавать новый объект вместо ранее уничтоженного. Никогда не было переустановки ссылок ни до, ни после C++20.

Gene 01.09.2024 23:27

@Gene Вы можете освободить хранилище и повторно использовать его, но не можете напрямую ссылаться на его участников. Вам пришлось пройти через std::launder, что сделало его практически непригодным для использования. C++ 20 устранил необходимость использования std::launder для этой цели.

doug 01.09.2024 23:56

@doug — это просто артефакт construct_at, который был представлен в C++20, то есть return ::new (voidify(*location)) T(std::forward<Args>(args)...); — он не требует, чтобы location был указателем на живой объект, в launder нет необходимости.

Gene 02.09.2024 00:13

@Genestd::launder требовался до C++20, потому что даже если вы освободили память для объекта и создали на его месте новый, вы не могли ссылаться на константы или ссылки и получать новые значения. По крайней мере технически. Но можно было бы и через отмывку. Начиная с C++20 в этом больше нет необходимости.

doug 02.09.2024 00:46
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
2
11
92
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Есть ли какое-либо объяснение тому, что делает std::pair?

Что касается вопроса выше, то результат следующего фрагмента кода, добавленного в конце вашего примера, скорее всего, вас удивит:

cout << "s1" << s1 << '\n'; // prints "s1 MySecond"
cout << "s2" << s2 << '\n'; // prints "s2 MySecond"

Никакой переустановки ссылок или каких-либо других дополнительных операций не было.

Простой факт заключается в том, что ссылки не являются указателями. Таким образом, копирование/замена данных где-то глубоко внутри сгенерированного кода сортировки вместо замены параметра второй пары, как вы ожидали, генерирует код, перезаписывающий ссылочные строки.

Спасибо. Это действительно то, что происходит. Таким образом, замена двух пар может перезаписать указанные значения. И это объясняет те сумасшедшие вещи, которые я увидел, когда перешел от указателей к ссылкам в своей базе кода. Я не уверен, каково будет использование такого поведения, но очевидно, что именно так работает пара.

doug 01.09.2024 23:28
Ответ принят как подходящий

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 01.09.2024 23:35

@doug Это вполне соответствует остальной части стандартной библиотеки и соответствует предполагаемому значению ссылки: любое действие, которое обычно применяется к объекту, вместо этого применяется к объекту, на который ссылается ссылка. А если это не предусмотрено, то следует использовать std::reference_wrapper. Ссылка ведет себя точно так же, как если бы на ее место был помещен объект, на который она ссылается.

user17732522 01.09.2024 23:39

Я действительно попробовал reference_wrapper в своей базе кода. Не удалось заставить его работать. Не знаю почему, ведь я использовал его раньше, но не тратил на это много времени. Думаю, я буду придерживаться подхода с указателем. Просто и понятно.

doug 01.09.2024 23:39

@doug «На самом деле, даже не определено поведение, поскольку порядок не указан: в самой паре операции четко определены. Проблема заключается только в вашем компараторе для std::sort, который не последовательно сравнивает элементы диапазона после операций замены. Если бы вместо этого вы сравнили первый элемент, он был бы четко определен.

user17732522 01.09.2024 23:41

Что ж, std::sort раньше никогда не приходилось задумываться о таких вещах. Я склонен думать о константных и ссылочных членах как об инвариантах конкретного объекта. Я думаю о присвоении объекта как о замене всего объекта новым, подчиняющимся тем же инвариантам, что и тип.

doug 01.09.2024 23:50

Я действительно попробовал reference_wrapper в своей базе кода. Не удалось заставить его работать. Похоже, ты делаешь что-то не так. Все, что вам нужно сделать в вашем примере, это заменить string& в аргументе пары на std::reference_wrapper<string> и добавить .get() или static_cast<std::string>() при доступе к обернутому объекту для печати, вам даже не нужно изменять лямбда обратного вызова, поскольку преобразование будет выполнено неявно.

sklott 01.09.2024 23:59

В моем примере работает нормально. В моей кодовой базе это просто не сработало: vector<vector<Stuff>> Понятия не имею, почему. Слишком сложно упростить, чтобы опубликовать здесь.

doug 02.09.2024 00:07

Другие вопросы по теме