Почему CLANG 3.5 on Linux дважды очищает "std :: string" при вызове DTOR при добавлении CTOR? - PullRequest
0 голосов
/ 02 августа 2020

Есть проект, ориентированный на использование C ++ 98 без дополнительных зависимостей, но он должен поддерживать динамически выделяемую память. Умные указатели недоступны, поэтому был добавлен код для ручной очистки. Подход состоит в том, чтобы явно установить переменные в NULL в CTOR, прочитать некоторые данные, во время которых память может выделяться динамически, перехватить любое возникающее исключение и очистить память по мере необходимости, вручную вызвав DTOR. Это должно в любом случае реализовать освобождение памяти на случай, если все прошло успешно, и было просто усилено мерами безопасности, чтобы проверить, была ли выделена память вообще или нет.

Ниже приводится наиболее актуальный доступный код на этот вопрос:

default_endian_expr_exception_t::doc_t::doc_t(kaitai::kstream* p__io, default_endian_expr_exception_t* p__parent, default_endian_expr_exception_t* p__root) : kaitai::kstruct(p__io) {
    m__parent = p__parent;
    m__root = p__root;
    m_main = 0;

    try {
        _read();
    } catch(...) {
        this->~doc_t();
        throw;
    }
}

void default_endian_expr_exception_t::doc_t::_read() {
    m_indicator = m__io->read_bytes(2);
    m_main = new main_obj_t(m__io, this, m__root);
}

default_endian_expr_exception_t::doc_t::~doc_t() {
    if (m_main) {
        delete m_main; m_main = 0;
    }
}

Самая важная часть заголовка следующая:

class doc_t : public kaitai::kstruct {
    public:
        doc_t(kaitai::kstream* p__io, default_endian_expr_exception_t* p__parent = 0, default_endian_expr_exception_t* p__root = 0);

    private:
        void _read();

    public:
        ~doc_t();

    private:
        std::string m_indicator;
        main_obj_t* m_main;
        default_endian_expr_exception_t* m__root;
        default_endian_expr_exception_t* m__parent;
    };

Код протестирован в трех разных средах , clang3.5_linux, clang7.3_osx и msvc141_windows_x64, чтобы явно генерировать исключения при чтении данных и при утечке памяти в этих условиях. Проблема в том, что это вызывает SIGABRT на CLANG 3.5 только для Linux. Наиболее интересными кадрами стека являются следующие:

<frame>
  <ip>0x577636E</ip>
  <obj>/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19</obj>
  <fn>std::basic_string&lt;char, std::char_traits&lt;char&gt;, std::allocator&lt;char&gt; &gt;::~basic_string()</fn>
</frame>
<frame>
  <ip>0x5ECFB4</ip>
  <obj>/home/travis/build/kaitai-io/ci_targets/compiled/cpp_stl_98/bin/ks_tests</obj>
  <fn>default_endian_expr_exception_t::doc_t::doc_t(kaitai::kstream*, default_endian_expr_exception_t*, default_endian_expr_exception_t*)</fn>
  <dir>/home/travis/build/kaitai-io/ci_targets/tests/compiled/cpp_stl_98</dir>
  <file>default_endian_expr_exception.cpp</file>
  <line>51</line>
</frame>

[...]

<frame>
  <ip>0x577636E</ip>
  <obj>/usr/lib/x86_64-linux-gnu/libstdc++.so.6.0.19</obj>
  <fn>std::basic_string&lt;char, std::char_traits&lt;char&gt;, std::allocator&lt;char&gt; &gt;::~basic_string()</fn>
</frame>
<frame>
  <ip>0x5ED17E</ip>
  <obj>/home/travis/build/kaitai-io/ci_targets/compiled/cpp_stl_98/bin/ks_tests</obj>
  <fn>default_endian_expr_exception_t::doc_t::~doc_t()</fn>
  <dir>/home/travis/build/kaitai-io/ci_targets/tests/compiled/cpp_stl_98</dir>
  <file>default_endian_expr_exception.cpp</file>
  <line>62</line>
</frame>

Строки 51 и 62 являются последними строками CTOR и DTOR, как указано выше, так что на самом деле закрывающие скобки. Похоже, что какой-то добавленный компилятором код просто пытается освободить поддерживаемый std::string два раза, один раз в DTOR и еще раз в CTOR, скорее всего, только при генерации исключения.

Это анализ вообще верен?

И если да, то это ожидаемое поведение C ++ в целом или только этого конкретного компилятора? Интересно, потому что другие компиляторы не SIGABRT, хотя код для всех одинаковый. Означает ли это, что разные компиляторы по-разному очищают не-указатели, такие как std::string? Как узнать, как ведет себя каждый компилятор?

Глядя на то, что говорит C ++ - стандарт , я ожидал, что std::string будет освобожден только CTOR из-за исключения:

C ++ 11 15.2 Конструкторы и деструкторы (2)

Объект любой продолжительности хранения, инициализация или уничтожение которого завершается исключением, будет иметь деструкторы выполняется для всех его полностью построенных подобъектов (за исключением вариантов членов объединенного класса), то есть для подобъектов, для которых главный конструктор (12.6.2) завершил выполнение, а деструктор еще не начал выполнение.

В данном случае уничтожение НЕ завершается исключением, а только построением. Но поскольку DTOR - это DTOR, он также предназначен для автоматической очистки? И если да, то в целом со всеми компиляторами или только с этим?

Надежен ли вообще вызов DTOR вручную?

Согласно моим исследованиям, вызов DTOR вручную не должен быть слишком плохим . Это неправильное выражение, и это большой отказ из-за того, что я вижу прямо сейчас? У меня сложилось впечатление, что если DTOR вызывается вручную, он просто должен быть совместим, чтобы называться таким образом. Какое из вышеперечисленных должно быть из моего понимания Он не работает только из-за автоматически сгенерированного кода компилятором, о котором я не знал.

Как это исправить?

Вместо того, чтобы вызывать DTOR вручную и запускать автоматически сгенерированный код, один следует просто использовать специальную функцию cleanUp, освобождающую память и устанавливающую указатели на NULL? Должно быть безопасно вызывать это в CTOR в случае исключения и всегда в DTOR, правильно? Или есть способ продолжать вызывать DTOR совместимым образом для всех компиляторов?

Спасибо!

Ответы [ 2 ]

0 голосов
/ 03 августа 2020

Вот упрощенный пример, который похож на ваш случай и делает поведение очевидным:

#include <iostream>

struct S {
    S() { std::cout << "S constructed\n";}
    ~S() { std::cout << "S destroyed\n";}
};

class Throws {
    S s;
public:
    Throws() {
        try {
            throw 42;
        } catch (int) {
            this->~Throws();
            throw;
        }
    }
};


int main() {
  try {
      Throws t;
  } catch (int) {}
}

Вывод:

S constructed
S destroyed
S destroyed

Демо с clang , демонстрация с g cc.

Пример демонстрирует неопределенное поведение, уничтожая один и тот же экземпляр S дважды. Поскольку деструктор мало что делает и, в частности, не имеет доступа к this, неопределенное поведение проявляется в том, что деструктор запускается дважды успешно, поэтому его можно легко наблюдать в действии.

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

[class.dtor] / 14 После выполнения тела деструктора и автоматического уничтожения любых объектов c продолжительность хранения, выделенная в теле, деструктор для класса X вызывает деструкторы для X прямых невариантных нестационарных c элементов данных, деструкторы для X невиртуальной прямой базы классов и, если X является наиболее производным классом (11.10.2), его деструктор вызывает деструкторы для виртуальных базовых классов X ...

0 голосов
/ 02 августа 2020

После вызова деструктора объект перестает существовать (оставляя вам неинициализированную память). Это означает, что деструкторы могут опускать «завершающие» записи в память, такие как установка указателя на ноль (объект перестает существовать, поэтому его значение не может быть прочитано). Это также означает, что в основном любая дальнейшая операция с этим объектом - это UB.

Люди предполагают некоторую свободу действий при уничтожении *this, если указатель this больше не используется. В вашем примере это не тот случай, так как деструктор вызывается дважды.

Мне известен ровно один случай, когда вызов деструктора вручную является правильным, и тот, где он наиболее верен: когда объект был создается с новым размещением (в этом случае не будет операции, которая автоматически вызывает деструктор). Наиболее правильный случай - это когда за уничтожением объекта сразу же следует повторная инициализация объекта через вызов place-new в том же месте.

Что касается вашего второго вопроса: почему вы хотите явно все равно вызвать деструктор? Насколько я понимаю, ваш код должен работать нормально, без каких-либо искажений:

default_endian_expr_exception_t::doc_t::doc_t(kaitai::kstream* p__io, default_endian_expr_exception_t* p__parent, default_endian_expr_exception_t* p__root)
  : kaitai::kstruct(p__io), m__parent(p__parent), m__root(p__root), m_main() {
    _read();
}

Объект инициализируется в допустимое состояние перед запуском конструктора, предоставленного пользователем. Если _read генерирует исключение, которое все еще должно иметь место (в противном случае исправьте _read!), И поэтому неявный вызов деструктора должен хорошо очистить все.

...