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

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

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

Исходная проблема возникает из-за кода, ответственного за сериализацию / десериализацию сообщений, и он использует 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;
}

person Dmitriy Kumshayev    schedule 28.05.2019    source источник
comment
Вы уверены? Печатает XYZ для меня, как я и ожидал - поскольку вы не изменяете строку v ...   -  person Barry    schedule 29.05.2019
comment
ideone.com/5EnKAZ не может дублироваться, однако const_cast предназначен не для этого.   -  person Retired Ninja    schedule 29.05.2019
comment
Я использовал g ++ 5.4.0 и clang ++ 4.0.0. Оба дают одинаковый результат ~~~ ~ / tmp $ g ++ -std = c ++ 14 x.cpp ~ / tmp $ ./a.out XYZ * # Z * # Z * YZ ~~~   -  person Dmitriy Kumshayev    schedule 29.05.2019
comment
clang 4.0: godbolt.org/z/SoNVEk g ++ 5.4: godbolt.org/z/rE73lI По-прежнему не происходит.   -  person Retired Ninja    schedule 29.05.2019
comment
Для меня пример печатает 4 строки: 1 - XYZ, 2 - * # Z 3 - * # Z, а последняя строка, о которой идет речь, печатает * YZ   -  person Dmitriy Kumshayev    schedule 29.05.2019
comment
Я не думаю, что здесь есть неопределенное поведение. const_cast работает до тех пор, пока данные не были объявлены как const. std::string не хранит свои данные как массив констант, насколько мне известно.   -  person Quimby    schedule 29.05.2019
comment
Воспроизведено здесь: onlinegdb.com/Hy78LVoTV   -  person Dmitriy Kumshayev    schedule 29.05.2019
comment
Престижность за фактическое воссоздание проблемы в подходящем минимальном воспроизводимом примере. Очень признателен!   -  person bolov    schedule 29.05.2019


Ответы (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)

Как вы заметили, семантика копирования при записи имеет тенденцию вызывать ошибки. Из-за этого, а также из-за того, что копирование строки довольно дешево (с учетом всех обстоятельств), реализация copy copy-on-write для 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 байтов:

введите описание изображения здесь

person Alecto Irene Perez    schedule 28.05.2019
comment
Спасибо! Я также добавил способ различать старую реализацию и новую реализацию, чтобы вы могли определить, какая из них используется. - person Alecto Irene Perez; 29.05.2019
comment
Я глубоко убежден, что строка COW - ужасно ошибочная идея на многих уровнях (на самом деле любая умная оптимизация в фундаментальном базовом блоке и любом простом классе - плохая идея), но это явно не может быть убедительным аргументом. Исправление ошибочного кода, связанного с const_cast, заключается в том, чтобы избежать преобразования, соблюдать безопасность констант, а также не испортить строковые данные через указатель. - person curiousguy; 29.05.2019
comment
Поведение COW было обесценено и удалено, и const_cast является допустимым решением в определенных ситуациях. Обновление компилятора исправит поведение COW, поэтому я рекомендовал его. - person Alecto Irene Perez; 29.05.2019