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

Почему указатели на встроенные функции разрешены?

У меня есть два вопроса:

1) Почему указатели на встроенные функции разрешены на С++? Я прочитал, что код встроенных функций просто копируется в оператор вызова функции, и в встроенных функциях отсутствует распределение памяти во время компиляции. Итак, почему указатель существует для встроенной функции, учитывая, что для встроенных функций нет фиксированного адреса памяти?

2) Рассмотрим приведенный ниже код:

inline void func()    
{
    int n=0;
    cout<<(&n);
} 

Должен ли он не печатать разные значения адреса n каждый раз, когда вызывается func()? [Поскольку я думаю, что каждый раз, когда код встроенной функции копируется, необходимо выполнить перераспределение локальных переменных (тогда как в случае нормальных функций происходит повторная инициализация)]

Я новичок, и я задал этот вопрос ради укрепления моей концепции. Пожалуйста, поправьте меня, если я где-то не прав.

4b9b3361

Ответ 1

1) Почему указатели на встроенные функции разрешены в С++?

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

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

Вы (и, возможно, материал, который вы прочитали) смешали две связанные и аналогично названные концепции.

Встроенная функция определена во всех единицах трансляции, которые ее используют, в то время как не-встроенная функция определена в одной единицы перевода только в соответствии с требованиями одного правила определения. Это то, что означает встроенное объявление функции; он ослабляет одно правило определения, но также дает дополнительное требование быть определенным во всех единицах перевода, которые его используют (что было бы невозможно, если odr не расслабился).

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

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

2) Если он не печатает разные значения адреса n каждый раз, когда вызывается функция func()?

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

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

Ответ 2

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

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

Преобразования кода (включая inlining) выполняются в "as-if" правило в С++, что в основном означает, что компилятор может преобразовать код так, как он хочет, пока выполнение "как есть", исходный код был выполнен как написанный. Это правило поддерживает оптимизацию в С++.

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

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

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

Если он не печатает разные значения адреса n каждый раз, когда вызывается func()?

Он может, n - это локальная переменная, основанная на местоположении стека при выполнении функции. Тем не менее, функция inline, она связана с привязкой, компоновщик объединит функции над единицами трансляции.


Как отмечено в комментариях;

... что если этот пример изменен на static int n, то каждый вызов функции должен печатать постоянное значение (в одном прогоне программы, конечно)... и это правда, является ли код inlined или нет.

Это опять-таки эффект требования привязки к локальной переменной n.

Ответ 3

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

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

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

Ответ 4

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

Если используется адрес функции, функция, скорее всего, не включена в окончательный исполняемый файл, по крайней мере, в GCC:

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

Документация GCC

Ответ 5

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

#include <iostream>

int foo(int (*fun)(int), int x) {
  return fun(x);
}
int succ(int n) {
  return n+1;
}
int main() {
  int c=0;
  for (int i=0; i<10000; ++i) {
    c += foo(succ, i);
  }
  std::cout << c << std::endl;
}

Здесь foo(succ, i) может быть в целом привязано только к i+1. И действительно, это похоже на : g++ -O3 -S создает код для функций foo и succ

_Z3fooPFiiEi:
.LFB998:
    .cfi_startproc
    movq    %rdi, %rax
    movl    %esi, %edi
    jmp *%rax
    .cfi_endproc
.LFE998:
    .size   _Z3fooPFiiEi, .-_Z3fooPFiiEi
    .p2align 4,,15
    .globl  _Z4succi
    .type   _Z4succi, @function
_Z4succi:
.LFB999:
    .cfi_startproc
    leal    1(%rdi), %eax
    ret
    .cfi_endproc

Но затем он генерирует код для main, который никогда не ссылается ни на одно из них, а просто включает в себя новый специализированный _GLOBAL__sub_I__Z3fooPFiiEi:

.LFE999:
    .size   _Z4succi, .-_Z4succi
    .section    .text.startup,"ax",@progbits
    .p2align 4,,15
    .globl  main
    .type   main, @function
main:
.LFB1000:
    .cfi_startproc
    movdqa  .LC1(%rip), %xmm4
    xorl    %eax, %eax
    pxor    %xmm1, %xmm1
    movdqa  .LC0(%rip), %xmm0
    movdqa  .LC2(%rip), %xmm3
    jmp .L5
    .p2align 4,,10
    .p2align 3
.L8:
    movdqa  %xmm2, %xmm0
.L5:
    movdqa  %xmm0, %xmm2
    addl    $1, %eax
    paddd   %xmm3, %xmm0
    cmpl    $2500, %eax
    paddd   %xmm0, %xmm1
    paddd   %xmm4, %xmm2
    jne .L8
    movdqa  %xmm1, %xmm5
    subq    $24, %rsp
    .cfi_def_cfa_offset 32
    movl    $_ZSt4cout, %edi
    psrldq  $8, %xmm5
    paddd   %xmm5, %xmm1
    movdqa  %xmm1, %xmm6
    psrldq  $4, %xmm6
    paddd   %xmm6, %xmm1
    movdqa  %xmm1, %xmm7
    movd    %xmm7, 12(%rsp)
    movl    12(%rsp), %esi
    call    _ZNSolsEi
    movq    %rax, %rdi
    call    _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_
    xorl    %eax, %eax
    addq    $24, %rsp
    .cfi_def_cfa_offset 8
    ret
    .cfi_endproc
.LFE1000:
    .size   main, .-main
    .p2align 4,,15
    .type   _GLOBAL__sub_I__Z3fooPFiiEi, @function
_GLOBAL__sub_I__Z3fooPFiiEi:
.LFB1007:
    .cfi_startproc
    subq    $8, %rsp
    .cfi_def_cfa_offset 16
    movl    $_ZStL8__ioinit, %edi
    call    _ZNSt8ios_base4InitC1Ev
    movl    $__dso_handle, %edx
    movl    $_ZStL8__ioinit, %esi
    movl    $_ZNSt8ios_base4InitD1Ev, %edi
    addq    $8, %rsp
    .cfi_def_cfa_offset 8
    jmp __cxa_atexit
    .cfi_endproc
.LFE1007:
    .size   _GLOBAL__sub_I__Z3fooPFiiEi, .-_GLOBAL__sub_I__Z3fooPFiiEi
    .section    .init_array,"aw"
    .align 8
    .quad   _GLOBAL__sub_I__Z3fooPFiiEi
    .local  _ZStL8__ioinit
    .comm   _ZStL8__ioinit,1,1

Таким образом, в этом случае фактическая программа даже не содержит указателя функции, указывающего на succ - компилятор обнаружил, что этот указатель всегда будет ссылаться на одну и ту же функцию в любом случае и поэтому смог устранить всю вещь без изменение поведения. Это может значительно повысить производительность, когда вы часто вызываете небольшие функции с помощью указателей функций. Это довольно распространенный метод на функциональных языках; компиляторы для таких языков, как O'Caml и Haskell, используют эту оптимизацию.


Отказ от ответственности: мои навыки сборки близки к несуществующим. Я вполне мог бы говорить здесь мусор.