Я придумал следующий пример, который демонстрирует неожиданное поведение. Я ожидаю, что после 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;
}
Я использовал g++ 5.4.0 и clang++ 4.0.0. Оба дают одинаковый результат ~~~ ~/tmp$ g++ -std=c++14 x.cpp ~/tmp$ ./a.out XYZ *#Z *#Z *YZ ~~~
clang 4.0: godbolt.org/z/SoNVEk g++ 5.4: godbolt.org/z/rE73lI Все еще не происходит.
Для меня пример печатает 4 строки: 1 - XYZ, 2 - *#Z 3 - *#Z и последняя рассматриваемая строка печатает *YZ
Я не думаю, что здесь есть неопределенное поведение. const_cast
работает до тех пор, пока данные не были объявлены константными. std::string
, насколько я знаю, не хранит свои данные в виде константного массива.
Воспроизведено здесь: onlinegdb.com/Hy78LVoTV
похвала за фактическое воссоздание проблемы в правильном минимальный воспроизводимый пример. Очень ценю!
Неожиданное поведение возникает из-за особенностей устаревшей реализации 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
использует мой компилятор?sizeof(std::string) == 32
(при компиляции для 64-битной версии)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 байтов:
Спасибо! Я также добавил способ различать старую реализацию и новую реализацию, чтобы вы могли определить, какая из них используется.
Я глубоко убежден, что COW-строка — ужасно ошибочная идея на многих уровнях (на самом деле любая умная оптимизация в фундаментальном базовом блоке и любом простом классе — плохая идея), но это явно не может быть убедительным аргументом. Исправление ошибочного кода, включающего const_cast
, состоит в том, чтобы избегать приведения, соблюдать безопасность констант, а также не путаться со строковыми данными с помощью указателя.
Поведение COW было обесценено и удалено, а const_cast является допустимым решением в определенных ситуациях. Обновление компилятора исправит поведение COW, поэтому я рекомендовал его.
Уверен? Печатает XYZ для меня, как я и ожидал, поскольку вы не изменяете строку
v
...