Как стать автором
Обновить
0
Content AI
Решения для интеллектуальной обработки информации

One Definition Rule, inline и неожиданные последствия их сочетания

Время на прочтение 4 мин
Количество просмотров 12K
C++ требует, чтобы любая функция была определена не более одного раза – One Definition Rule, ODR. Как только вы определяете функцию с одним и тем же именем и сигнатурой в разных единицах трансляции (файлах .cpp), вы получаете индикацию ошибки на этапе линковки.

inline функции обычно определяются в заголовочных файлах (.h), чтобы все единицы трансляции могли видеть реализацию функции и подставить ее по месту вызова. Соответственно, как только вы включите заголовочный файл с такой функцией в более чем одну единицу трансляции, ODR будет формально нарушено, но… никакой индикации ошибки вы не получите.

Почему и какие неожиданные последствия это может иметь?
Почему – вопрос относительно хорошо известный (например). С одной стороны, запретить описанную выше ситуацию нельзя из практических соображений – нужно, чтобы реализация функции была доступна из всех вызывающих ее единиц трансляции, иначе подстановка может оказаться невозможной. С другой стороны, ODR нарушается и хорошо бы на это отреагировать.

Реагировать можно двумя способами – сообщением об ошибке или молчанием. В этом конкретном случае линкер выбирает второе. Как только линкер видит более одной inline функции с одним и тем же именем и одной и той же сигнатурой, он считает, что это одна и та же функция, и выбирает одну из них на свое усмотрение.

Так формальное нарушение ODR формально устраняется.

ВДРУГ открывается широкий простор для труднообнаружимых дефектов. Политика, используемая линкером, предполагает, что функция действительно одна и та же, т.е. идентично реализована. Нас ждет пример – запаситесь чипсами и читайте дальше.

Медленно помешивая, добавим немного препроцессора (например, так сделано для обработчика ошибок в ATL):

//CommonFile.h
__declspec( noinline ) //заботливо поставлен noinline
inline void HandleErrorCondition( int condition )
{
#ifndef OVERRIDE_STANDARD_HANDLING
     _exit(1);
#else
    CustomHandleErrorCondition( condition );
#endif
}


//StaticLib.h
#include <CommonFile.h>
inline void SomeUsefulFunction()
{
    //blahblahblah
    HandleErrorCondition( 0 );
}

//StaticLib.cpp
#include <StaticLib.h>
blahblahblah, вызов SomeUsefulFunction()

//Executable.cpp
void CustomHandleErrorCondition( int condition )
{
     throw MyCustomException( condition );     
}
#define OVERRIDE_STANDARD_HANDLING
#include <StaticLib.h>
blahblahblah, вызов SomeUsefulFunction()

//V2UncmUgaGlyaW5nIC0gd3d3LmFiYnl5LnJ1L3ZhY2FuY3k=

StaticLib.cpp компилируется в статическую библиотеку StaticLib.lib, затем Executable.cpp компилируется в исполняемый файл (.exe или .dll – все равно) и статически влинковывает StaticLib.lib.

Сигнатура HandleErrorCondition() содержит __declspec(noinline) – атрибут Visual C++, который говорит компилятору, что подставлять реализацию этой функции не нужно никогда. Это сделано специально, чтобы компилятор не подставил реализацию функции и реализацию можно было заменить позже. Visual C++ подчиняется.

Хитрый план™, ради которого нужна эта кухня, очевиден: если разработчика все устраивает, будет использован обработчик по умолчанию. Если обработчик по умолчанию не устраивает, можно установить свой.

Будет ли это работать? Какая реализация HandleErrorCondition() – с вызовом _exit() или с вызовом CustomHandleErrorCondition()– будет вызываться?

Неизвестно.

Когда компилятор компилирует StaticLib.cpp, он включает в объектный файл (StaticLib.obj) первую реализацию – с вызовом _exit(). Когда компилятор компилирует Executable.cpp, он включает в объектный файл (Executable.obj) вторую реализацию – с вызовом CustomHandleErrorCondition().

При линковке возникает описанная выше ситуация с нарушением ODR, но теперь две inline функции, идентичные с точки зрения политики, используемой линкером, имеют разные реализации. Линкер выберет какую-то одну и не факт, что выбор не будет меняться от одной линковки к другой.

ВДРУГ ваша программа работает не так, как вы планировали. Что особенно приятно, поведение в этом примере будет отличаться только при обработке ошибок, т.е. в относительно редких ситуациях и не факт, что их не забудут проверить.

Описанное поведение (выбор одной из функций) демонстрирует Visual C++. Отдельные читатели уже готовятся написать едкий комментарий, но зря. В соответствии со стандартом С++ ISO/IEC 14882:2003(E), параграф 3.2/5, в описанной ситуации поведение не определено. Соответственно, линкер не обязан ни давать каких-либо разумных результатов, ни давать одни и те же результаты при повторных линковках тех же объектных файлов. В каких-то случаях поведение будет тем, что вы ожидали, в каких-то, возможно, нет. Так что Visual C++ невиновен.

Пора отметить, что пример выглядит довольно искусственным и кривым. Опять же, __declspec(noinline) – возможность, специфичная для Visual C++. Есть масса других способов оказаться ровно в той же ситуации.

Например, в двух разных заголовках могут случайно оказаться разные inline функции с одной и той же сигнатурой. Если не возникнет ситуации, когда оба заголовка включаются в один и тот же файл .cpp, вы не получите ошибки компиляции и далее окажетесь в ситуации нарушения ODR. Еще разные файлы .cpp могут быть скомпилированы с разными значениями какой-нибудь настройки компилятора, которая меняет поведение кода. Снова та же ситуация. #pragma pack тоже может внести посильный вклад.

Наконец, если __declspec(noinline) отсутствует, в каких-то случаях компилятор может подставить реализацию функции, соответствующую настройкам компилятора и символам препроцессора, заданным для соответствующей единицы трансляции.

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

Решение одно – если ваш код использует inline функции, позаботьтесь о том, чтобы компиляция разных единиц трансляции выполнялась так, чтобы поведение этих функций оставалось неизменным. Настройки компиляции должны быть одними и теми же, символы препроцессора – также одними и теми же.

В описанном выше случае нужно определить OVERRIDE_STANDARD_HANDLING в проекте StaticLib и пересобрать его.

Берегите себя.

Дмитрий Мещеряков
Департамент продуктов для ввода данных
Теги:
Хабы:
+33
Комментарии 27
Комментарии Комментарии 27

Публикации

Информация

Сайт
www.contentai.ru
Дата регистрации
Дата основания
Численность
101–200 человек
Местоположение
Россия

Истории