Есть ли еще применение inline?

Я считал, что inline устарел, потому что я прочитал здесь:

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

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

Этот вопрос не касается:

Принимая во внимание, что компилятор может inline по желанию, поэтому inline здесь не поможет: Где можно inline использовать, чтобы не предлагать изменить скомпилированный код?


person Jonathan Mee    schedule 22.04.2015    source источник
comment
@MatthieuM. Я посмотрел: stackoverflow.com/questions/1759300/ перед публикацией, я почувствовал, что это вопрос о влиянии inline на встраиваемый код. Я специально заявляю, что не хотел об этом знать. Я хотел узнать об альтернативном использовании inlines. На мой взгляд, эти два вопроса очень разные.   -  person Jonathan Mee    schedule 22.04.2015
comment
Ну, лично я обнаружил, что ваш вопрос дублировался; поскольку после того, как вы установили, что inline бесполезно намекать на встраивание, остается только семантика, и это именно то, что. Когда мне следует писать ключевое слово «inline» для функции / метода? было около. Но поскольку ваш вопрос был повторно открыт, по-видимому, другие думали так же. Обратите внимание: если вы просмотрели другие вопросы, рекомендуется связать их и объяснить, почему вы думаете, что ваш вопрос отличается.   -  person Matthieu M.    schedule 23.04.2015
comment
Я проголосовал против из-за серьезного несоответствия между названием вопроса и фактическим вопросом. Вы спрашиваете, есть ли еще использование встроенного кода?, На который ответ - да, из-за ODR, и вы также спрашиваете, где inline можно использовать для принудительного изменения скомпилированного кода?, На который нет ответа, если вы не попадете в некоторые специализированные параметры компилятора. Затем вы говорите, что на самом деле вы все время знали первое, а настоящий вопрос - это второе.   -  person Steve Jessop    schedule 23.04.2015
comment
@MatthieuM. Я добавил раздел, посвященный некоторым вопросам, на которые я смотрел, а также тому, чем, на мой взгляд, отличается этот вопрос. Я ценю вашу конструктивную критику и надеюсь, что в итоге вопрос стал лучше.   -  person Jonathan Mee    schedule 23.04.2015
comment
@SteveJessop Я считаю, что приведенные ниже ответы адекватно объясняют, как inline можно использовать для принудительного изменения скомпилированного кода; в этом код не может быть скомпилирован без inline из-за ODR. Я размышлял, как мне повысить точность ответа на этот вопрос, но в конце концов это показалось мне лучшим, что я уже мог сделать. Я приветствовал бы предложение, если вы чувствуете, что можно сказать что-то, что уместно дифференцирует вопрос.   -  person Jonathan Mee    schedule 23.04.2015
comment
@JonathanMee: Хорошо, если да, поскольку ODR является приемлемым ответом, то это обман вопроса, на который вы ссылались, поскольку ответ на него всегда требуется ODR. Небольшая вариация в способе задания вопроса, который ранее был пропущен, не создает новый вопрос, и если вы пытаетесь задать вопрос, нужно ли нам по-прежнему использовать встроенный по причинам, указанным в этом ответе от 2009 года. , что делать с ODR ?, опять же, это все еще правда? ИМО - обманщик.   -  person Steve Jessop    schedule 23.04.2015


Ответы (2)


Я постараюсь как можно лучше объяснить свое «тайное понимание».

Здесь есть два совершенно разных понятия. Один из них - это способность компилятора заменять вызов функции, повторяя тело функции непосредственно в месте вызова. Другой - возможность определения функции более чем в одной единице перевода (= более чем в одном .cpp файле).

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

Надеюсь, мы разобрались с явным отношением inlineinlining. В текущем коде его нет.

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

file1.cpp

int f() { return 42; }

int main()
{ return f(); }

file2.cpp

int f() { return 42; }

Эта команда:

> gcc file1.cpp file2.cpp

Выдает ошибку компоновщика с жалобой на то, что символ f определен дважды.

Однако, если вы помечаете функцию ключевым словом inline, она конкретно сообщает компилятору и компоновщику: «Ребята, убедитесь, что несколько идентичных определений этой функции не приводят к каким-либо ошибкам!»

Таким образом, будет работать следующее:

file1.cpp

inline int f() { return 42; }

int main()
{ return f(); }

file2.cpp

inline int f() { return 42; }

Компиляция и связывание этих двух файлов вместе не приведет к ошибкам компоновщика.

Обратите внимание, что определение f не обязательно должно быть дословно в файлах. Вместо этого он может поступать из файла заголовка #included:

f.hpp

inline int f() { return 42; }

file1.cpp

#include "f.hpp"

int main()
{ return f(); }

file2.cpp

#include "f.hpp"

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


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

C ++ следует отдельной модели компиляции, где компилятор не имеет доступа к объектным файлам, кроме тех, которые он в настоящее время создает. Следовательно, чтобы иметь возможность встроить функцию, ее определение должно быть частью текущей единицы перевода. Если вы хотите иметь возможность встроить его в несколько единиц перевода, его определение должно быть во всех из них. Обычно это приводит к множественной ошибке определения. Поэтому, если вы поместите свою функцию в заголовок и #include ее определение повсюду, чтобы включить ее встраивание повсюду, вы должны пометить ее как inline, чтобы предотвратить множественные ошибки определения.

Обратите внимание, что даже сегодня, хотя компилятор будет встроить любую функцию, которую он считает нужной, он все еще должен иметь доступ к определению этой функции. Таким образом, хотя ключевое слово inline не требуется в качестве подсказки «пожалуйста, вставьте это», вы все равно можете обнаружить, что вам нужно использовать его, чтобы включить компилятор для выполнения встраивания, если он решит это сделать. Без него вы не сможете получить определение в единицу перевода, а без определения компилятор просто не сможет встроить функцию.

Компилятор не может. Компоновщик может. Современные методы оптимизации включают генерацию кода во время компоновки (также известную как оптимизация всей программы), при которой оптимизатор запускается для всех объектных файлов как часть процесса связывания перед фактическим связыванием. На этом этапе, конечно, доступны все определения функций, и встраивание вполне возможно без использования единственного ключевого слова inline где-либо в программе. Но такая оптимизация обычно требует больших затрат времени на сборку, особенно для крупных проектов. Имея это в виду, полагаться только на LTCG для встраивания может быть не лучшим вариантом.


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

  • Может быть определен в нескольких единицах перевода, не вызывая ошибок компоновщика
  • Должен быть определен в каждой единице перевода, в которой он используется
  • Все его определения должны быть идентичными для токена для токена и сущности для сущности.

Ключевое слово inline превращает функцию во встроенную функцию. Другой способ пометить функцию как встроенную - определить (а не просто объявить) ее непосредственно в определении класса. Такая функция является встроенной автоматически, даже без ключевого слова inline.

person Angew is no longer proud of SO    schedule 22.04.2015
comment
Отличное объяснение в последней части ответа! - person Luchian Grigore; 22.04.2015
comment
Очень хорошее объяснение, я все еще использовал встроенные функции для очень маленьких функций, которые программа вызывала много раз. - person Vinz; 22.04.2015
comment
@tepples Спасибо за предложение отредактировать, но я специально оставил его до конца. Мне пришлось довести до ума, что inline не касается встраивания функций в смысле компилятора, и я чувствую, что встроенные функции делают это еще более мрачным. - person Angew is no longer proud of SO; 22.04.2015
comment
Следует отметить обязательную диагностику при нарушении inline правил. (в частности, что не требуется диагностика). Кроме того, в любом сообщении на inline должны упоминаться моменты, в которых это неявно (функции шаблона, функции, определенные в теле класса / структуры). Наконец, namespace{} и его использование вместо inline (особенно в компиляторе с идентичным сворачиванием comdat (или ICF)) и плюсы / минусы в сравнении (плюсы: менее хрупкие. Минусы: ICF часто является итеративным, и каждый уровень псевдо-встроенного требует итерация) - person Yakk - Adam Nevraumont; 22.04.2015
comment
Кроме того, constexpr функции и =delete;'d функции неявно встроены. - person T.C.; 22.04.2015
comment
@Yakk Шаблоны функций не являются явно inline; у них есть отдельное освобождение от ODR. - person T.C.; 22.04.2015
comment
@ T.C .: Я думаю, что тщательная настройка всегда лучше статических компиляторов. Есть много ситуаций, когда программисты будут знать то, чего не могут компиляторы, и наоборот, особенно в отношении того, как часто будут выполняться различные фрагменты кода при ожидаемом использовании. Возможно, более полезной директивой было бы сообщить компиляторам, какие фрагменты кода будут выполняться очень часто или редко, а не встраивать или нет, но я думаю, что компиляторам всегда будет нужна помощь для получения оптимальных результатов. - person supercat; 22.04.2015
comment
@Angew: Итак, когда я пишу inline int f () {return 42; } int main () {return f (); } в file1.cpp & inline int f () {return 42; } в file2.cpp обе функции рассматриваются как отдельные (не идентичные) функции. Правильно? - person Destructor; 25.09.2015
comment
@PravasiMeet Нет, на самом деле это одна и та же функция, в том-то и дело. Он имеет одинаковый адрес во всех .cpp файлах (= единицах трансляции) и использует одни и те же статические переменные (если есть). Это просто одна функция, но ее тело должно присутствовать во всех единицах трансляции, которые к ней относятся (например, вызывать ее или принимать ее адрес). - person Angew is no longer proud of SO; 25.09.2015
comment
@Angew: другими словами, чтобы потенциально встроить функцию (заменить ее вызовы ее телом на сайтах вызовов), вызываемую в нескольких .cpp файлах (единицах перевода), компилятору необходимо определение функции в каждом .cpp файл, но компоновщик допускает только одно определение функции в программе (Одно правило определения), поэтому мы используем ключевое слово inline, чтобы обойти ограничение компоновщика. Итог: ключевое слово inline успокаивает компоновщик, чтобы разрешить встраивание функции, когда функция вызывается в нескольких .cpp файлах. Я правильно понял? - person Maggyero; 05.01.2016
comment
@Maggyero Практически да. Чисто формально сам C ++ также запрещает множественные определения, поэтому inline успокаивает эту часть C ++. Но эта часть находится в C ++ в основном из-за связывания, поэтому на практике то, что вы говорите, является точным. - person Angew is no longer proud of SO; 05.01.2016
comment
@Angew: Хорошо. Два вопроса: 1. В своем последнем комментарии вы говорите, что определение функции должно присутствовать в .cpp файлах, которые ссылаются на функцию, поэтому не только функция вызывает f(), но также функция адреса f и &f, но какой смысл в адресах функций, если они не встроены (заменены телом функции), как вызовы функций? 2. Помимо разрешения встраивания функций в несколько файлов .cpp, существуют ли другие ситуации, в которых используется ключевое слово inline? (Я не могу представить себе другую цель этого ключевого слова, допускающую несколько одинаковых определений.) - person Maggyero; 05.01.2016
comment
@Maggyero 1. Стандарт говорит в терминах odr-used, и взятие адреса составляет odr-use, поэтому определение должно быть там. Может быть, это больше для последовательности, чем для необходимости. 2. Практически inline позволяет вам поместить функцию в файл заголовка. Будет ли он встроен или нет, зависит от компилятора; это также позволяет, например, существовать библиотекам только для заголовков. - person Angew is no longer proud of SO; 05.01.2016
comment
Было бы полезно объяснить, чем и почему это отличается от использования static. Проблема в том, что вы начинаете с довольно бесполезного примера дублирования определения, который предполагает, что речь идет об определении другого кода в разных местах, что просто не работает с inline. Я бы посоветовал начать с того, что это отличается от использования ключевого слова static. Возможно, стоит отметить, что современные линкеры способны встраивать. - person Frax; 28.04.2018

inline в настоящее время в основном является просто спецификатором внешней ссылки по причинам, которые вы указали.

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

//header.h
inline void foo() {}
void goo() {}

//cpp1.cpp
#include "header.h"

//cpp2.cpp
#include "header.h"

// foo is okay, goo breaks the one definition rule (ODR)

Фактически принудительное встраивание функций зависит от компилятора, некоторые из них могут иметь поддержку через определенные attributes, pragmas, (__forceinline) или еще много чего.

Проще говоря, он позволяет вам определять функции в заголовках, не нарушая ODR ...

person Luchian Grigore    schedule 22.04.2015
comment
Почему он всегда может работать с внешней связью? Это потому, что компилятор / компоновщик видит, что в противном случае ошибки определения произошли бы? - person Panzercrisis; 22.04.2015
comment
@Panzercrisis: многие компоновщики могут поддерживать концепцию слабого определения, так что, если символ строго определен, компоновщик будет игнорировать любые слабые определения и кричать при втором сильном определении, но если он только слабо определен, компоновщик произвольно выберет слабое определение не жалуясь, если многие из них существуют. Это очень полезная концепция, которую также можно использовать для сглаживания различий API (хотя она используется не так часто, как могла бы). Например, передача значений с плавающей запятой в регистры FPU будет быстрее, чем передача их в регистры главного процессора ... - person supercat; 22.04.2015
comment
... на ARM с FPU, но для руки без FPU потребуется передача регистров главного процессора. Можно заставить всех вызывающих работать со всеми методами, указав, что функции, которые принимают вещи в регистрах FPU, должны быть определены с измененным именем, и слабо определить функцию с неизмененным именем, которая загружает регистры ЦП в регистры FPU и вызывает измененную версию. Код, который ожидает передать данные другой функции в регистрах FPU, будет вызывать функцию с измененным именем и слабо определять функцию с измененным именем, которая загружает данные в регистры ЦП ... - person supercat; 22.04.2015
comment
... и называет неизменным. Код, который ничего не знает о FPU, просто использовал бы неизмененное имя и ожидал, что что-то будет в регистрах процессора. В этом сценарии любой код может вызывать любой тип метода. Насколько мне известно, никакой ABI для ARM на самом деле не работает таким образом в отношении использования FPU, но такие подходы часто полезны для сглаживания аналогичных проблем. - person supercat; 22.04.2015
comment
@supercat Я не уверен, слежу ли за вами ... Было бы слишком сложно создать отдельный вопрос и самому на него ответить? Я думаю, что пример того, о чем вы говорите, был бы действительно полезен, потому что я не уверен, что понимаю слабое определение, поэтому все последующее становится все более туманным. - person Jonathan Mee; 22.04.2015