C++ требует, чтобы любая функция была определена не более одного раза – One Definition Rule, ODR. Как только вы определяете функцию с одним и тем же именем и сигнатурой в разных единицах трансляции (файлах .cpp), вы получаете индикацию ошибки на этапе линковки.
inline функции обычно определяются в заголовочных файлах (.h), чтобы все единицы трансляции могли видеть реализацию функции и подставить ее по месту вызова. Соответственно, как только вы включите заголовочный файл с такой функцией в более чем одну единицу трансляции, ODR будет формально нарушено, но… никакой индикации ошибки вы не получите.
Почему и какие неожиданные последствия это может иметь?
Почему – вопрос относительно хорошо известный (например). С одной стороны, запретить описанную выше ситуацию нельзя из практических соображений – нужно, чтобы реализация функции была доступна из всех вызывающих ее единиц трансляции, иначе подстановка может оказаться невозможной. С другой стороны, ODR нарушается и хорошо бы на это отреагировать.
Реагировать можно двумя способами – сообщением об ошибке или молчанием. В этом конкретном случае линкер выбирает второе. Как только линкер видит более одной inline функции с одним и тем же именем и одной и той же сигнатурой, он считает, что это одна и та же функция, и выбирает одну из них на свое усмотрение.
Так формальное нарушение ODR формально устраняется.
ВДРУГ открывается широкий простор для труднообнаружимых дефектов. Политика, используемая линкером, предполагает, что функция действительно одна и та же, т.е. идентично реализована. Нас ждет пример – запаситесь чипсами и читайте дальше.
Медленно помешивая, добавим немного препроцессора (например, так сделано для обработчика ошибок в ATL):
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 и пересобрать его.
Берегите себя.
Дмитрий Мещеряков
Департамент продуктов для ввода данных
inline функции обычно определяются в заголовочных файлах (.h), чтобы все единицы трансляции могли видеть реализацию функции и подставить ее по месту вызова. Соответственно, как только вы включите заголовочный файл с такой функцией в более чем одну единицу трансляции, ODR будет формально нарушено, но… никакой индикации ошибки вы не получите.
Почему и какие неожиданные последствия это может иметь?
Почему – вопрос относительно хорошо известный (например). С одной стороны, запретить описанную выше ситуацию нельзя из практических соображений – нужно, чтобы реализация функции была доступна из всех вызывающих ее единиц трансляции, иначе подстановка может оказаться невозможной. С другой стороны, ODR нарушается и хорошо бы на это отреагировать.
Реагировать можно двумя способами – сообщением об ошибке или молчанием. В этом конкретном случае линкер выбирает второе. Как только линкер видит более одной inline функции с одним и тем же именем и одной и той же сигнатурой, он считает, что это одна и та же функция, и выбирает одну из них на свое усмотрение.
Так формальное нарушение ODR формально устраняется.
ВДРУГ открывается широкий простор для труднообнаружимых дефектов. Политика, используемая линкером, предполагает, что функция действительно одна и та же, т.е. идентично реализована. Нас ждет пример – запаситесь чипсами и читайте дальше.
//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 и пересобрать его.
Берегите себя.
Дмитрий Мещеряков
Департамент продуктов для ввода данных