Всегда ли вызов чистых виртуальных функций косвенно из конструктора неопределенным поведением?

Я работаю над созданием Cppcheck для AIX с компилятором xlC (см. предыдущий вопрос). Все классы проверки являются производными от класса Check, конструктор которого регистрирует каждый объект в глобальном списке:

check.h

class Check {
public:
    Check() {
        instances().push_back(this);
        instances().sort();
    }
    static std::list<Check *> &instances();
    virtual std::string name() const = 0;
private:
    bool operator<(const Check *other) const {
        return (name() < other->name());
    }
};

checkbufferoverrun.h

class CheckBufferOverrun: public Check {
public:
    // ...
    std::string name() const {
        return "Bounds checking";
    }
};

Проблема, с которой я столкнулся, связана с вызовом instances().sort(). sort() вызовет Check::operator<(), который вызывает Check::name() для каждого указателя в статическом instances() списке, но экземпляр Check, который был только что добавлен в список, еще не полностью запустил свой конструктор (потому что он все еще находится внутри Check::Check()). Следовательно, вызов ->name() для такого указателя до завершения конструктора CheckBufferOverrun должен быть неопределенным.

Это действительно неопределенное поведение, или мне здесь не хватает тонкости?

Обратите внимание: я не думаю, что вызов sort() строго необходим, но в результате Cppcheck запускает все свои средства проверки в детерминированном порядке. Это влияет только на вывод в том порядке, в котором обнаружены ошибки, что приводит к сбою некоторых тестовых примеров, поскольку они ожидают вывода в определенном порядке.

Обновление. Вышеупомянутый вопрос (в основном) остается в силе. Однако я думаю, что настоящая причина, по которой вызов sort() в конструкторе не вызывал проблем (например, сбой при вызове чистой виртуальной функции), заключается в том, что Check::operator<(const Check *) никогда не вызывается sort()! Вместо этого sort(), похоже, сравнивает указатели. Это происходит как в g++, так и в xlC, что указывает на проблему с самим кодом Cppcheck.


person Greg Hewgill    schedule 01.02.2011    source источник
comment
Добавьте параметр строкового имени в конструктор Check, и потомки предоставят значение во время построения. Сделайте name() не виртуальным и верните значение, переданное конструктором. Остальная часть конструктора Check, показанного здесь, может оставаться как есть.   -  person Rob Kennedy    schedule 02.02.2011
comment
@ Роб Кеннеди: Это хорошее решение, я предлагаю его разработчикам.   -  person Greg Hewgill    schedule 02.02.2011
comment
Да, и подумайте, стоит ли использовать набор вместо списка для экземпляров - это может быть быстрее, чем явный вызов sort после каждой вставки. Кстати, ваш оператор ‹никогда не будет вызван сортировкой, потому что он сравнивает Check с Check *.   -  person Axel    schedule 02.02.2011


Ответы (4)


Да, это не определено. Стандарт специально говорит об этом в 10.4 / 6.

Функции-члены могут быть вызваны из конструктора (или деструктора) абстрактного класса; эффект выполнения виртуального вызова (10.3) чистой виртуальной функции прямо или косвенно для объекта, создаваемого (или уничтожаемого) из такого конструктора (или деструктора), не определен.

person Rob Kennedy    schedule 01.02.2011

Верно, что вызов чистой виртуальной функции из конструктора всегда является неопределенным поведением.

Нельзя предполагать, что виртуальный указатель установлен, пока конструктор не запустится полностью (закрытие "}"), и, следовательно, любой вызов виртуальной функции (или чистой виртуальной функции) должен быть настроен во время самой компиляции (статически привязан вызов).

Теперь, если виртуальная функция является чистой виртуальной функцией, компилятор обычно вставляет свою собственную реализацию для такой чистой виртуальной функции, поведение которой по умолчанию заключается в создании ошибки сегментации. Стандарт не диктует, какой должна быть реализация чистой виртуальной функции, но большинство компиляторов C ++ используют вышеупомянутый стиль.

Если ваш код не вызывает никакого вреда во время выполнения, то он не вызывается в указанной последовательности вызовов. Если бы вы могли опубликовать код реализации для ниже 2 функций

instances().push_back(this);
instances().sort();

тогда, возможно, это поможет увидеть, что происходит.

person Viren    schedule 01.02.2011
comment
Имена файлов в вопросе указывают на фактический полный код на Github. - person Greg Hewgill; 02.02.2011
comment
Извините, я не знал, что это ссылка. Я просматриваю файл Check.h, но не вижу функции sort (), объявленной в этом заголовке. :( - person Viren; 02.02.2011
comment
ИЗВИНИТЕ. Виноват. это функция списка. - person Viren; 02.02.2011
comment
Ни один компилятор не может генерировать ошибку сегментации. Что это за чушь? - person Lightness Races in Orbit; 08.02.2012
comment
это то, что я видел при использовании ms vc ++ ide еще в дни своего симбиана. кроме того, для более логичного объяснения см. это: stackoverflow.com/questions/99552/ Этот вопрос не совсем связан с этим ответом, но вызов чистой виртуальной функции приведет к сбою сегмента. - person Viren; 09.02.2012

Пока построение объекта не завершено, чистая виртуальная функция не может быть вызвана. Однако, если он объявлен чистым виртуальным в базовом классе A, а затем определен в B (производном от A), конструктор C (производный от B) может вызвать его, поскольку построение B завершено.

В вашем случае используйте вместо этого статический конструктор:

class check {
private Check () { ... }
public:
    static Check* createInstance() {
        Check* check = new Check();
        instances().push_back(check);
        instances().sort();
    }
...
}
person Axel    schedule 01.02.2011
comment
Вы не можете создать экземпляр ABC, поэтому оператор this не должен даже компилироваться: Check * check = new Check (); - person Viren; 02.02.2011
comment
Не могли бы вы подробнее рассказать об этом? Я не уверен, что понимаю, что вы имеете в виду (или вы не понимаете пример). Кроме того, в моем коде отсутствует двоеточие. - person Axel; 02.02.2011
comment
О, понятно ... Вы имели в виду класс Check, а не ABC из приведенного мной примера. Да, нельзя создать экземпляр Check - я упустил из виду, что Check - это базовый класс, объявляющий чистую виртуальную функцию. - person Axel; 02.02.2011

Я думаю, ваша настоящая проблема в том, что вы объединили две вещи: базовый класс Checker и некоторый механизм для регистрации (производных) экземпляров Check.

Среди прочего, это не особенно надежно: я могу использовать ваши классы Checker, но я могу зарегистрировать их по-другому.

Возможно, вы могли бы сделать что-то вроде этого: Checker получает защищенный ctor (в любом случае он абстрактный, поэтому только производные классы должны вызывать Checker ctor).

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

Для создания экземпляра Checkerregister и предоставления его подклассам Checker вы используете любой одноточечный шаблон или механизм внедрения зависимостей.

Один из простых способов сделать это - использовать статический метод getCheckerRegister в Checker.

Итак, подкласс Checker может выглядеть так:

class CheckBufferOverrun: public Check {protected: CheckBufferOverrun: Check ("Проверка границ") {// поскольку у каждого производного есть имя, почему бы просто не передать его как аргумент? } public: CheckBufferOverrun makeCheckBufferOverrun () {CheckBufferOverrun, который = новый CheckBufferOverrun ();

   // get the singleton, pass it something fully constructed
   Checker.getCheckerRegister.register(that) ;
   return that;
}

Если похоже, что в итоге получится много шаблонного кода, напишите шаблон. Если вас беспокоит, что, поскольку каждый экземпляр шаблона в C ++ является реальным и уникальным классом, напишите не шаблонный базовый класс, который будет регистрировать любые производные от Checker'а.

person tpdi    schedule 01.02.2011
comment
Да, механизм регистрации, используемый в Cppcheck, кажется попыткой минимизировать количество кода соединителя, необходимого для реализации нового подкласса Check и его использования в основном цикле проверки. Теоретически все, что нужно сделать, - это объявить и реализовать производный класс от Check, создать глобальный статический экземпляр, и остальная часть программы начнет его использовать. Я бы, наверное, не выбрал такой путь. - person Greg Hewgill; 02.02.2011