Разве С++ inline не является обязательным?

У меня есть класс, в котором есть встроенный член, но позже я решил, что хочу удалить реализацию из заголовков, поэтому я переместил тело членов функций в файл cpp. Сначала я просто оставил встроенную подпись в файле заголовка (sloppy me), и программа не смогла правильно связать. Затем я исправил заголовок, и все это отлично работает, конечно.

Но не был ли он полностью необязательным?

В коде:

Во-первых:

//Class.h
class MyClass
{
   void inline foo()
   {}
};

Далее изменено на (не будет ссылка):

//Class.h
class MyClass
{
   void inline foo();
};

//Class.cpp
void MyClass::foo()
{}

А потом (будет отлично работать):

//Class.h
class MyClass
{
   void foo();
};

//Class.cpp
void MyClass::foo()
{}

Я думал, что inline не является обязательным, и предположил, что я могу пройти с предупреждением о моей неряшливости, но не ожидал ошибки связывания. Какая правильная/стандартная вещь, которую должен сделать компилятор в этом случае, заслужил ли я свою ошибку в соответствии со стандартом?

Ответ 1

В самом деле, существует одно правило определения, указывающее, что встроенная функция должна быть определена в каждой используемой единицы перевода. Далее перечислены детали Gory. Первый 3.2/3:

Каждая программа должна содержать ровно одно определение каждой не-встроенной функции или объекта, которая используется в этой программе; не требуется диагностика. Определение может явно отображаться в программе, оно может быть найдено в стандартной или определяемой пользователем библиотеке или (если необходимо), оно неявно определено (см. 12.1, 12.4 и 12.8). Встроенная функция должна быть определена в каждой единицы перевода, в которой она используется.

И, конечно, 7.1.2/4:

Встроенная функция должна быть определена в каждой единицы перевода, в которой она используется, и должна иметь точно такое же определение в каждом случае (3.2). [Примечание: вызов встроенной функции может быть встречен до того, как ее определение появится в блоке перевода. ] Если функция с внешней связью объявлена ​​встроенной в одну единицу перевода, она должна быть объявлена ​​встроенной во все единицы перевода, в которых она отображается; диагностика не требуется. Встроенная функция с внешней связью должна иметь один и тот же адрес во всех единицах перевода. Статическая локальная переменная во внешней встроенной функции всегда относится к одному и тому же объекту. Строковый литерал во внешней встроенной функции - это тот же объект в разных единицах перевода.

Однако, если вы определяете свою функцию в определении класса, она неявно объявляется как функция inline. Это позволит вам включить определение класса, содержащее это тело встроенной функции, несколько раз в вашей программе. Так как функция имеет связь external, любое ее определение будет относиться к той же функции (или более gory - к тому же entity).

Горы подробности о моей претензии. Первый 3.5/5:

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

Тогда 3.5/4:

Имя, имеющее область пространства имен, имеет внешнюю связь, если это имя [...] именованного класса (раздел 9) или неназванный класс, определенный в объявлении typedef, в котором класс имеет имя typedef для целей привязки.

Это "имя для целей связи" - это забавная вещь:

typedef struct { [...] } the_name;

Поскольку теперь у вас есть несколько определений одного и того же объекта в ваших программах, другое дело ODR, как правило, ограничивает вас. 3.2/5 следует за скучным материалом.

В программе может быть несколько определений типа класса (раздел 9), тип перечисления (7.2), встроенная функция с внешней связью (7.1.2) [...] в программе, при условии, что каждое определение появляется в другой единицы перевода, и при условии, что определения удовлетворяют следующим требованиям. Учитывая такой объект с именем D, определенный более чем в одной единицы перевода, тогда

  • каждое определение D должно состоять из одной и той же последовательности токенов; и
  • в каждом определении D соответствующие имена, просмотренные в соответствии с 3.4, относятся к сущности, определенной в определении D, или должны ссылаться на один и тот же объект после разрешения перегрузки (13.3) и после согласования частичного шаблона специализация (14.8.3) [...]

Теперь я отрезал некоторые неважные вещи. Вышеупомянутые два важных момента, чтобы помнить о встроенных функциях. Если вы определяете внешнюю встроенную функцию несколько раз, но определяете ее по-разному, или если вы ее определяете, а имена, используемые в ней, разрешают разные сущности, то вы выполняете поведение undefined.

Правило, согласно которому функция должна быть определена в каждом TU, в котором она используется, легко запомнить. И это то же самое легко запомнить. Но как насчет разрешения имени? Вот пример. Рассмотрим статическую функцию assert_it:

static void assert_it() { [...] }

Теперь, поскольку static даст ему внутреннюю связь, когда вы включите его в несколько единиц перевода, тогда каждое определение будет определять другой объект. Это означает, что вам не разрешено использовать assert_it из встроенной функции extern, которая будет определена несколько раз в программе: поскольку происходит то, что встроенная функция будет ссылаться на один объект с именем assert_it в одном TU, но к другому объекту с тем же именем в другом ТУ. Вы обнаружите, что все это скучная теория, и компиляторы, вероятно, не будут жаловаться, но я нашел этот пример, в частности, показывает связь между ODR и сущностями.


Далее следует вернуться к вашей конкретной проблеме.

Ниже приведены те же самые вещи:

struct A { void f() { } };
struct A { inline void f(); }; void A::f() { } // same TU!

Но это другое, поскольку функция не является встроенной. Вы нарушите ODR, поскольку у вас есть несколько определений f, если вы включаете заголовок более одного раза

struct A { void f(); }; void A::f() { } // evil!

Теперь, если вы помещаете inline в объявление f внутри класса, но затем опускаете его определение в заголовке, тогда вы нарушаете 3.2/37.1.2/4, который говорит то же самое, только более подробно), так как функция не определена в этой единицы перевода!

Обратите внимание, что в C (C99) inline имеет другую семантику, чем в С++. Если вы создаете внешнюю встроенную функцию, сначала прочитайте хорошую бумагу (желательно стандарт), так как это действительно сложно в C (в принципе, для любого используемого встроенного определения функции потребуется другое определение не встроенной функции в другом Статические встроенные функции TU в C легко обрабатываются. Они ведут себя как любая другая функция, кроме обычного подсказки "встроенной подстановки". Static inline как в C, так и в С++ служит только как подсказка inline-substitution. уже создаст другую сущность в любое время (из-за внутренней привязки), inline просто добавит подсказку inline-substitution - не более.

Ответ 2

Независимо от того, действительно ли этот метод встроен, он находится на собственном усмотрении компилятора. Однако наличие ключевого слова inline также повлияет на связь метода.

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

В качестве альтернативы вы можете просто ждать litb, чтобы предоставить детали gory через час или около того;)

Ответ 3

Указание на заметку: когда метод объявлен inline, его определение ДОЛЖНО быть вместе с его объявлением.

Ответ 4

Относительно ответа harshath.jr, метод не должен быть объявлен встроенным, если его определение имеет ключевое слово "встроенное", и это определение доступно в том же заголовке, то есть:

class foo
{
  void bar();
};

inline void foo::bar()
{
  ...
}

Это полезно для условной вставки метода в зависимости от того, является ли сборка "debug" или "release" следующим образом:

// Header - foo.h

class foo
{
  void bar();  // Conditionally inlined.
};

#ifndef FOO_DEBUG
# include "foo.inl"
#endif

"Встроенный" файл может выглядеть так:

// Inline Functions/Methods - foo.inl
#ifndef FOO_DEBUG
# define FOO_INLINE inline
#else
# define FOO_INLINE
#endif

FOO_INLINE void foo::bar()
{
  ...
}

и реализация может понравиться следующим образом:

// Implementation file - foo.cpp
#ifdef FOO_DEBUG
# include "foo.inl"
#endif

...

Это не совсем красиво, но он использует его, когда агрессивная inline становится головной болью отладки.