Встроенные функции-члены в C ++ - PullRequest
14 голосов
/ 02 марта 2009

ISO C ++ говорит, что встроенное определение функции-члена в C ++ такое же, как и объявление его встроенным. Это означает, что функция будет определена в каждой единице компиляции, в которой используется функция-член. Однако, если вызов функции не может быть встроен по какой-либо причине, экземпляр функции должен создаваться «как обычно». (http://msdn.microsoft.com/en-us/library/z8y1yy88%28VS.71%29.aspx) Проблема, связанная с этим определением, заключается в том, что в нем не указано, в какой единице перевода он будет создан. Проблема, с которой я столкнулся, заключается в том, что при обращении к двум объектным файлам в одной статической библиотеке, каждая из которых имеет ссылку на некоторую встроенную функцию-член, которая не может быть встроенной, компоновщик может «выбрать» произвольный объектный файл в качестве источника для определения. Этот конкретный выбор может вводить ненужные зависимости. (между прочим)

Например: В статической библиотеке

A.h:

class A{
  public:
    virtual bool foo() { return true; }
};

U1.cpp:

A a1;

U2.cpp:

A a2;

и множество зависимостей

В другом проекте main.cpp:

#include "A.h"

int main(){
  A a;
  a.foo();
  return 0;
}

Второй проект относится к первому. Как мне узнать, какое определение будет использовать компилятор и, следовательно, какие объектные файлы с их зависимостями будут связаны? Есть ли что-нибудь, что стандарт говорит по этому вопросу? (Пробовал, но не смог найти это)

Спасибо

Редактировать: так как я видел, что некоторые люди неправильно понимают, в чем заключается вопрос, я хотел бы подчеркнуть: Если компилятор решил создать символ для этой функции (и в этом случае это произойдет из-за «Виртуальность», будет несколько (видимых извне) экземпляров в другом объектном файле, какое определение (из какого объектного файла?) выберет компоновщик?)

Ответы [ 5 ]

8 голосов
/ 02 марта 2009

Только мои два цента. Речь идет не о виртуальных функциях в частности, а о встроенных функциях и функциях-членах в целом. Может быть, это полезно.

C ++

Что касается стандарта C ++, встроенная функция должна быть определена в каждой единице перевода, в которой она используется. И нестатическая встроенная функция будет иметь одинаковые статические переменные в каждой единице перевода и один и тот же адрес. Для достижения этого компилятор / компоновщик должен будет объединить несколько определений в одну функцию. Поэтому всегда помещайте определение встроенной функции в заголовок - или не помещайте его объявление в заголовок, если вы определяете его только в файле реализации (".cpp") (для функции, не являющейся членом), потому что если вы Если бы кто-то использовал его, вы бы получили ошибку компоновщика о неопределенной функции или о чем-то подобном.

Это отличается от не встроенных функций, которые должны быть определены только один раз во всей программе ( правило одного определения ). Для встроенных функций несколько определений, как указано выше, являются вполне нормальным случаем. И это не зависит от того, является ли вызов встроенным или нет. Правила о встроенных функциях все еще имеют значение. Придерживается ли компилятор Microsoft этих правил или нет - я не могу вам сказать. Если он придерживается стандарта в этом отношении, то он будет. Тем не менее, я мог бы предположить, что некоторые комбинации с использованием виртуальных, DLL и различных TU могут быть проблематичными. Я никогда не проверял это, но я думаю, что нет никаких проблем.

Для функций-членов, если вы определяете свою функцию в классе, она неявно встроена. И поскольку оно появляется в заголовке, автоматически выполняется правило о том, что оно должно быть определено в каждой единице перевода, в которой оно используется. Однако если вы определяете функцию вне класса и в файле заголовка (например, потому что существует циклическая зависимость с кодом между ними), то это определение должно быть встроенным, если вы включаете соответствующий файл более одного раза, чтобы Избегайте ошибок множественного определения, генерируемых компоновщиком. Пример файла f.h:

struct f {
    // inline required here or before the definition below
    inline void g();
};

void f::g() { ... }

Это будет иметь тот же эффект, что и определение прямо в определении класса.

C99

Обратите внимание, что правила для встроенных функций более сложны для C99, чем для C ++. Здесь встроенная функция может быть определена как встроенное определение , из которых может существовать более одной во всей программе. Но если используется такое (встроенное) определение (например, если оно вызывается), то должно быть также точно одним внешним определением во всей программе, содержащейся в другой единице перевода. Обоснование этого (цитата из PDF-файла, объясняющая обоснование нескольких функций C99):

Встраивание в C99 расширяет спецификацию C ++ двумя способами. Во-первых, если функция объявлена ​​встроенной в одной единице перевода, ее не нужно объявлять встроенной в любой другой единице перевода. Это позволяет, например, использовать библиотечную функцию, которая должна быть встроена в библиотеку, но доступна только через внешнее определение в другом месте. Альтернатива использования функции-оболочки для внешней функции требует дополнительного имени; и это также может отрицательно повлиять на производительность, если переводчик фактически не выполняет встроенную замену.

Во-вторых, требование, чтобы все определения встроенной функции были «точно такими же», заменялось требованием, чтобы поведение программы не зависело от того, реализован ли вызов с видимым встроенным определением или от внешнего определения , функции. Это позволяет встроенному определению быть специализированным для его использования в определенной единице перевода. Например, внешнее определение библиотечной функции может включать проверку некоторого аргумента, которая не требуется для вызовов, выполняемых из других функций в той же библиотеке. Эти расширения предлагают некоторые преимущества; и программисты, которые обеспокоены совместимостью, могут просто соблюдать более строгие правила C ++.

Почему я включаю C99 сюда? Потому что я знаю, что компилятор Microsoft поддерживает некоторые вещи C99. Так что на этих страницах MSDN некоторые вещи могут быть взяты и из C99 - но я ничего особенного не понял. Следует соблюдать осторожность при его чтении и применении техник к собственному коду C ++, предназначенному для переносимого C ++. Вероятно, информирование о том, какие части специфичны для C99, а какие нет.

Хорошим местом для тестирования небольших фрагментов C ++ на соответствие стандарту является онлайн-компилятор comeau . Если он отклонен, можно быть уверенным, что он не соответствует стандарту.

7 голосов
/ 02 марта 2009

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

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

4 голосов
/ 02 марта 2009

AFAIK, не существует стандартного определения того, как и когда компилятор C ++ встроит вызов функции. Обычно это «рекомендации», которым компилятор никоим образом не обязан следовать. Фактически, разные пользователи могут хотеть различного поведения. Один пользователь может заботиться о скорости, в то время как другой может заботиться о небольшом размере сгенерированного объектного файла. Кроме того, компиляторы и платформы разные. Некоторые компиляторы могут применять более разумный анализ, а некоторые нет. Некоторые компиляторы могут генерировать более длинный код из встроенного кода или работать на платформе, где вызовы слишком дороги и т. Д.

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

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

3 голосов
/ 02 марта 2009

Если компилятор решил создать символ для этой функции (и в этом случае из-за «виртуальности» будет несколько (внешне видимых) экземпляров в другом объектном файле, какое определение (из какого объекта файл?) компоновщик выберет?)

Определение, присутствующее в соответствующей единице перевода. И единица перевода не может, и я повторяю, не может иметь только одно такое определение. В стандарте об этом ясно.

[...] компоновщик может «выбрать» произвольный объектный файл в качестве источника для определения.

РЕДАКТИРОВАТЬ: Чтобы избежать дальнейшего недопонимания, позвольте мне прояснить мою точку зрения: в соответствии с моим прочтением стандарта, возможность иметь несколько определений в разных TU не дает нам любой практическое плечо. Под практическим я имею в виду даже немного отличающиеся реализации. Теперь, если все ваши TU имеют одно и то же определение, зачем беспокоиться, из какого TU выбрано это определение?

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

3.2 Одно правило определения:

5 Может быть несколько определений типа класса (раздел 9), концепта (14.9), концептуальной карты (14.9.2), типа перечисления (7.2), встроенной функции с внешней связью (7.1.2), [...]

Прочитайте это вместе с

3 [...] Встроенная функция должна быть определена в каждой единице перевода, в которой она используется.

Это означает, что функция будет определяться в каждой единице компиляции [...]

и

7.1.2 Спецификаторы функций

2 Объявление функции (8.3.5, 9.3, 11.4) со встроенным спецификатором объявляет встроенную функцию. Встроенный спецификатор указывает реализации, что внутренняя замена тела функции в точке вызова должна быть предпочтительнее обычного механизма вызова функции. Реализация не требуется для выполнения этой внутренней замены в точке вызова; однако, даже если эта внутренняя замена не указана, другие правила для встроенных функций, определенных в 7.1.2, все еще должны соблюдаться.

3 Функция, определенная в определении класса, является встроенной функцией. Встроенный спецификатор не должен появляться в объявлении функции области видимости блока. [Footnote: 82] Если встроенный спецификатор используется в объявлении друга, это объявление должно быть определением, или функция должна быть ранее объявлена ​​встроенной.

и сноска:

82) Ключевое слово inline не влияет на связь функции. § 7.1.2 138

а также:

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

Distilled: можно иметь несколько определений, но они должны выглядеть одинаково в каждой единице и адресе перевода - но это не дает вам особого повода для радости. Поэтому наличие нескольких определений в разных единицах перевода не определено (примечание: я еще не говорю, что вы вызываете UB).

Что касается virtual штучки - никаких вставок не будет. Период.

Стандарт гласит:

  • То же самое объявление должно быть доступно
  • Должно быть одно определение

С MSDN :

Данная встроенная функция-член должна объявляться одинаково в каждой единице компиляции. Это ограничение заставляет встроенные функции вести себя так, как если бы они были экземплярами функций. Кроме того, должно быть ровно одно определение встроенной функции.

Ваш A.h содержит определение класса и определение члена foo().

U1.cpp и U2.cpp оба определяют два разных объекта класса A.

Вы создаете еще один A объект в main(). Это просто отлично.

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

2 голосов
/ 02 марта 2009

Не вставляйте свои функции, если хотите, чтобы они были скомпилированы в определенную библиотеку.

...