Профессионалам высокого уровня, гуру, магам и волшебникам языка
Лирическое отступление
Несколько слов о названии, призванном объединить статьи подобного рода. Оно, естественно, появилось не случайно, однако и не совсем соответствует сути.
Мои статьи будут рассчитаны на тех, кто уже более-менее знаком с языком
А причины на самом деле не такие уж и сверхъестественные. Зачастую свою роль играет человеческий фактор. К примеру, прочитав какую-либо книгу для начинающих, в которых, как известно, многие нюансы не объясняются, а иногда даже не упоминаются, дабы упростить восприятие основ языка, читатель додумывает недостающие вещи самостоятельно из соображений a la «по-моему, это логично». Отсюда возникают крупицы недопонимания, иногда приводящие к довольно серьезным ошибкам, ну, а в большинстве случаев просто мешающие успешному прохождению различного рода олимпиад по С++ :)
Итак, миф первый
Как известно, в языке
Подставляемая функция объявляется достаточно просто:
inline void foo(int & _i)
{
_i++;
}
Но речь сейчас не об этом. Мы рассмотрим использование подставляемых методов класса. И начнем с небольшого примера, по вине которого и может возникать данный миф.
Все вы знаете, что определения методов класса можно писать как снаружи класса, так и внутри, и подставляемые функции здесь не исключение. Притом функции, определенные прямо внутри класса, автоматически становятся подставляемыми и ключевое слово inline в таком случае излишне. Рассмотрим пример (использую struct вместо class только для того чтобы не писать public):
// InlineTest.cpp
#include <cstdlib>
#include <iostream>
struct A
{
inline void foo() { std::cout << "A::foo()" << std::endl; }
};
struct B
{
inline void foo();
};
void B::foo()
{
std::cout << "B::foo()" << std::endl;
}
int main()
{
A a; B b;
a.foo();
b.foo();
return EXIT_SUCCESS;
}
В данном примере все отлично, и на экране мы видим заветные строки:
A::foo()
B::foo()
Причем компилятор действительно подставил тела методов в места их вызовов.
Наконец-то мы подобрались к сути сегодняшней статьи. Проблемы начинаются в тот момент, когда мы (соблюдая «хороший стиль программирования») разделяем класс на cpp- и h-файлы:
// A.h
#ifndef _A_H_
#define _A_H_
class A
{
public:
inline void foo();
};
#endif // _A_H_
// A.cpp
#include "A.h"
#include <iostream>
void A::foo()
{
std::cout << "A::foo()" << std::endl;
}
// main.cpp
#include <cstdlib>
#include <iostream>
#include "A.h"
int main()
{
A a;
a.foo();
return EXIT_SUCCESS;
}
На стадии линковки получаем ошибку вроде такой (зависит от компилятора — у меня MSVC):
main.obj: error LNK2001: unresolved external symbol «public: void __thiscall A::foo (void)» (? foo@A@@QAEXXZ)
Почему?! Всё достаточно просто: определение подставляемого метода и её вызов находятся в разных единицах трансляции! Не совсем уверен, как именно это устроено внутренне, но я вижу эту проблему так:
если бы это был обычный метод, то в единице трансляции main.obj компилятор бы поставил нечто вроде call XXXXX, а позже уже компоновщик заменил бы XXXXX на конкретный адрес метода A::foo() из единицы трансляции A.obj (конечно же, я всё упростил, но суть не меняется).
В нашем же случае мы имеем дело с inline-методом, то есть вместо вызова компилятор должен подставить непосредственно текст метода. Так как определение находится в другой единице трансляции, компилятор оставляет эту ситуацию на попечение компоновщика. Здесь есть два момента: во-первых, «сколько места должен оставить компилятор для подстановки тела метода?», а во-вторых, в единице трансляции A.obj метод A::foo() нигде не используется, причем метод объявлен как inline (а значит там, где нужно было, компилятор должен был скопировать тело метода), поэтому отдельная скомпилированная версия этого метода в итоговый объектный файл не попадает вообще.
В подтверждение пункта 2 приведу немного дополненный пример:
// A.h
#ifndef _A_H_
#define _A_H_
class A
{
public:
inline void foo();
void bar();
};
#endif // _A_H_
// A.cpp
#include "A.h"
#include <iostream>
void A::foo()
{
std::cout << "A::foo()" << std::endl;
}
void A::bar()
{
std::cout << "A::bar()" << std::endl;
foo();
}
// main.cpp
#include <cstdlib>
#include <iostream>
#include "A.h"
int main()
{
A a;
a.foo();
return EXIT_SUCCESS;
}
Теперь всё работает, как и должно, благодаря тому, что inline-метод A::foo() вызывается в неподставляемом методе A::bar(). Если взглянуть на ассемблерный код итогового бинарника, можно увидеть, что, как и раньше, отдельной скомпилированной версии метода foo() нет (то есть у метода нет своего адреса), а тело метода скопировано непосредственно в места вызова.
Как выйти из этой ситуации? Очень просто: подставляемые методы нужно определять непосредственно в header-файле (не обязательно внутри объявления класса). При этом ошибки повторного определения не возникает, так как компилятор говорит компоновщику игнорировать ошибки ODR (One Definition Rule), а компоновщик в свою очередь оставляет только одно определение в результирующем бинарном файле.
Заключение
Надеюсь, хоть кому-то моя первая статья станет полезной и чуточку поможет достигнуть полного осознания такого странного и местами противоречивого, но, безусловно, интересного языка программирования, как