Неожиданное поведение с участием const_cast - PullRequest
3 голосов
/ 29 мая 2019

Я придумал следующий пример, который демонстрирует неожиданное поведение.Я ожидал бы, что после 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;
}

1 Ответ

4 голосов
/ 29 мая 2019

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

Неожиданное поведение возникает из-за причуд в устаревшей реализации 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)

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

enter image description here

...