Подтвердить что ты не робот

Встроенные функции-члены в С++

ISO С++ говорит, что встроенное определение функции-члена в С++ такое же, как объявление его с помощью inline. Это означает, что функция будет определена в каждом модуле компиляции, в котором используется функция-член. Однако, если вызов функции не может быть встроен по какой-либо причине, функция должна создаваться "как обычно". (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;
}

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

Спасибо

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

4b9b3361

Ответ 1

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

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

Ответ 2

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

С++

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

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

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

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

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

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

C99

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

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

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

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

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

Ответ 3

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

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

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

Ответ 4

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

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

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

РЕДАКТИРОВАТЬ: Чтобы избежать каких-либо дальнейших недоразумений, позвольте мне высказать свою точку зрения: согласно моему чтению стандарта способность иметь множественное определение в разных 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) с встроенным спецификатором объявляет встроенную функцию. Спецификатор inline указывает на реализацию, что встроенная подстановка тела функции в точке вызова предпочтительнее обычного механизма вызова функции. Реализация не требуется для выполнения этой встроенной подстановки в точке вызова; однако, даже если эта встроенная замена опущена, другие правила для встроенных функций, определенных в 7.1.2, все равно должны соблюдаться.

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

и сноска:

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

а также:

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

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

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

В стандарте говорится:

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

От MSDN:

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

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

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

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

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

Ответ 5

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