Неожиданное поведение, связанное с const_cast

Я придумал следующий пример, который демонстрирует неожиданное поведение. Я ожидаю, что после push_back все, что есть в векторе, будет там. Похоже, компилятор каким-то образом решил повторно использовать память, используемую str.

Может ли кто-нибудь объяснить, что происходит в этом примере? Это действительный код С++?

Исходная проблема возникает из-за кода, отвечающего за сериализацию/десериализацию сообщений, и он использует const_cast для удаления константности. Заметив неожиданное поведение этого кода, я создал этот упрощенный пример, который пытается продемонстрировать проблему.

#include <vector>
#include <iostream>
#include <string>
using namespace std;
int main()
{
    auto str = std::string("XYZ"); // mutable string
    const auto& cstr(str);         // const ref to it

    vector<string> v;
    v.push_back(cstr);

    cout << v.front() << endl;  // XYZ is printed as expected

    *const_cast<char*>(&cstr[0])='*'; // this will modify the first element in the VECTOR (is this expected?)
    str[1]='#';  //

    cout << str << endl;  // prints *#Z as expected
    cout << cstr << endl; // prints *#Z as expected
    cout << v.front() << endl; // Why *YZ is printed, not XYZ and not *#Z ?

    return 0;
}

Уверен? Печатает XYZ для меня, как я и ожидал, поскольку вы не изменяете строку v...

Barry 29.05.2019 00:00
ideone.com/5EnKAZ не может дублироваться, однако const_cast не для этого.
Retired Ninja 29.05.2019 00:02

Я использовал g++ 5.4.0 и clang++ 4.0.0. Оба дают одинаковый результат ~~~ ~/tmp$ g++ -std=c++14 x.cpp ~/tmp$ ./a.out XYZ *#Z *#Z *YZ ~~~

Dmitriy Kumshayev 29.05.2019 00:06

clang 4.0: godbolt.org/z/SoNVEk g++ 5.4: godbolt.org/z/rE73lI Все еще не происходит.

Retired Ninja 29.05.2019 00:13

Для меня пример печатает 4 строки: 1 - XYZ, 2 - *#Z 3 - *#Z и последняя рассматриваемая строка печатает *YZ

Dmitriy Kumshayev 29.05.2019 00:16

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

Quimby 29.05.2019 00:17

Воспроизведено здесь: onlinegdb.com/Hy78LVoTV

Dmitriy Kumshayev 29.05.2019 00:30

похвала за фактическое воссоздание проблемы в правильном минимальный воспроизводимый пример. Очень ценю!

bolov 29.05.2019 01:11
Стоит ли изучать 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 называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
3
8
142
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Понимание ошибки

Неожиданное поведение возникает из-за особенностей устаревшей реализации std::string. Старые версии GCC реализованы std::string с использованием семантики копирование при записи. Это умная идея, но она вызывает ошибки, подобные той, которую вы видите. Это означает, что GCC пытался определить std::string так, чтобы внутренний строковый буфер копировался только в случае изменения нового std::string. Например:

std::string A = "Hello, world";
std::string B = A; // No copy occurs (yet)
A[3] = '*'; // Copy occurs now because A got modified.

Однако когда вы берете постоянный указатель, копирование не происходит, потому что библиотека предполагает, что строка не будет изменена с помощью этого указателя:

std::string A = "Hello, world"; 
std::string B = A;
std::string const& A_ref = A;

const_cast<char&>(A_ref[3]) = '*'; // No copy occurs (your bug)

Как вы заметили, семантика копирование при записи имеет тенденцию вызывать ошибки. Из-за этого, а также потому, что копирование строки довольно дешево (учитывая все обстоятельства), реализация копирования копирование при записи для std::string была обесценивается и удаляется в GCC 5.

Итак, почему вы видите эту ошибку, если используете GCC 5? Вероятно, вы компилируете и компонуете более старую версию стандартной библиотеки C++ (в которой копирование при записи все еще является реализацией std::string). Это то, что вызывает ошибку для вас.

Проверьте, для какой версии стандартной библиотеки C++ выполняется компиляция, и, если возможно, обновите свой компилятор.

Как узнать, какую реализацию std::string использует мой компилятор?

  • Новая реализация GCC: sizeof(std::string) == 32 (при компиляции для 64-битной версии)
  • Старая реализация GCC: sizeof(std::string) == 8 (при компиляции для 64-битной версии)

Если ваш компилятор использует старую реализацию std::string, то sizeof(std::string) будет таким же, как sizeof(char*), потому что std::string реализован как указатель на блок памяти. Блок памяти — это тот, который фактически содержит такие вещи, как размер и емкость строки.

struct string { //Old data layout
    size_t* _data; 
    size_t size() const {
        return *(data - SIZE_OFFSET); 
    }
    size_t capacity() const {
        return *(data - CAPACITY_OFFSET); 
    }
    char const* data() const {
        return (char const*)_data; 
    }
};

С другой стороны, если вы используете более новую реализацию std::string, то sizeof(std::string) должно быть 32 байта (в 64-битных системах). Это связано с тем, что более новая реализация хранит размер и емкость строки в самом std::string, а не в данных, на которые он указывает:

struct string { // New data layout
    char* _data;
    size_t _size;
    size_t _capacity; 
    size_t _padding; 
    // ...
}; 

Чем хороша новая реализация? Новая реализация имеет ряд преимуществ:

  • Доступ к размеру и емкости может выполняться быстрее (поскольку оптимизатор, скорее всего, сохранит их в регистрах или, по крайней мере, в кеше).
  • Поскольку std::string составляет 32 байта, мы можем воспользоваться оптимизацией малых строк. Оптимизация малых строк позволяет хранить строки длиной менее 16 символов в пространстве, обычно занимаемом _capacity и _padding. Это позволяет избежать выделения кучи и быстрее для большинства случаев использования.

Ниже мы видим, что GDB использует старую реализацию std::string, потому что sizeof(std::string) возвращает 8 байтов:

Спасибо! Я также добавил способ различать старую реализацию и новую реализацию, чтобы вы могли определить, какая из них используется.

Alecto Irene Perez 29.05.2019 00:56

Я глубоко убежден, что COW-строка — ужасно ошибочная идея на многих уровнях (на самом деле любая умная оптимизация в фундаментальном базовом блоке и любом простом классе — плохая идея), но это явно не может быть убедительным аргументом. Исправление ошибочного кода, включающего const_cast, состоит в том, чтобы избегать приведения, соблюдать безопасность констант, а также не путаться со строковыми данными с помощью указателя.

curiousguy 29.05.2019 01:12

Поведение COW было обесценено и удалено, а const_cast является допустимым решением в определенных ситуациях. Обновление компилятора исправит поведение COW, поэтому я рекомендовал его.

Alecto Irene Perez 29.05.2019 03:39

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