web-dev-qa-db-fra.com

Pourquoi les pointeurs vers les fonctions en ligne sont-ils autorisés?

J'ai deux questions:

1) Pourquoi les pointeurs vers les fonctions en ligne sont-ils autorisés en C++? J'ai lu que le code des fonctions en ligne est simplement copié dans l'instruction d'appel de fonction et qu'il n'y a pas d'allocation de mémoire au moment de la compilation dans les fonctions en ligne. Alors, pourquoi un pointeur peut-il exister vers une fonction en ligne, étant donné qu'il n'y a pas d'adresse mémoire fixe pour les fonctions en ligne?

2) Considérez le code ci-dessous:

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

Ne doit-il pas afficher différentes valeurs de l'adresse de n à chaque fois que func() est appelée? [Parce que je pense que chaque fois que le code de fonction en ligne est copié, une réallocation des variables locales doit être effectuée (alors que dans le cas des fonctions normales, la réinitialisation a lieu)]

Je suis un débutant et j'ai posé cette question pour le renforcement de mon concept. Veuillez me corriger si je me trompe quelque part.

50
Madhuchhanda Mandal

1) Pourquoi les pointeurs vers les fonctions en ligne sont autorisés en c ++?

Parce que les fonctions en ligne sont des fonctions comme les autres, et les pointer est l'une des choses que vous pouvez faire avec les fonctions. Les fonctions en ligne ne sont tout simplement pas spéciales à cet égard.

J'ai lu que le code des fonctions en ligne est simplement copié dans l'instruction d'appel de fonction et qu'il n'y a pas d'allocations de mémoire de temps de compilation dans les fonctions en ligne.

Vous (et peut-être le matériel que vous avez lu) avez mélangé deux concepts apparentés et portant le même nom.

Une fonction en ligne est définie dans toutes les unités de traduction qui l'utilisent, tandis qu'une fonction non en ligne est définie dans une unité de traduction uniquement comme l'exige la règle de définition unique. C'est ce que signifie une déclaration en ligne d'une fonction; il assouplit la règle de définition unique, mais donne également l'exigence supplémentaire d'être défini dans toutes les unités de traduction qui l'utilisent (ce qui n'aurait pas été possible si l'od n'était pas assoupli).

L'expansion en ligne (ou inlining) est une optimisation, où un appel de fonction est évité en copiant la fonction appelée dans le cadre de l'appelant. Un appel de fonction peut être développé en ligne, que la fonction ait été déclarée en ligne ou non. Et une fonction qui a été déclarée en ligne n'est pas nécessairement développée en ligne.

Cependant, une fonction ne peut pas être développée en ligne dans une unité de traduction où elle n'est pas définie (sauf si l'optimisation du temps de liaison effectue l'expansion). Par conséquent, l'exigence d'être définie dans toutes les UT autorisées par la déclaration en ligne permet également l'expansion en ligne de la fonction en permettant à la fonction d'être définie dans toutes les UT qui l'invoquent. Mais l'optimisation n'est pas garantie.

2) Ne doit-il pas imprimer différentes valeurs d'adresse de n à chaque appel de func ()?

L'expansion en ligne fait que les variables locales se trouvent dans le cadre de l'appelant, oui. Mais leur emplacement sera différent indépendamment de l'expansion si les appels proviennent de trames distinctes.

Il existe généralement une version régulière non développée de toute fonction qui a été développée en ligne. Si l'adresse d'une fonction est prise, elle pointera vers cette fonction non développée. Si le compilateur peut prouver que tous les appels à une fonction sont en ligne, le compilateur peut choisir de ne pas fournir du tout la version non développée. Cela nécessite que la fonction ait un lien interne, et prendre l'adresse de la fonction rend généralement une telle preuve très difficile, voire impossible.

58
eerorika

Le mot-clé inline était à l'origine une indication pour le compilateur que vous, le programmeur, pensez que cette fonction est candidate à l'inlining - le compilateur n'est pas tenu d'honorer cela.

Dans l'utilisation moderne, cela n'a rien ou presque à voir avec l'incrustation - les compilateurs modernes intègrent librement (ou non) les fonctions "derrière vous", celles-ci font partie des techniques d'optimisation.

Les transformations de code (y compris l'inlining) sont effectuées sous la règle "as-if" en C++ , ce qui signifie essentiellement que le compilateur peut transformer le code comme il le souhaite, tant que l'exécution est "comme si" le code d'origine était exécuté tel qu'il était écrit. Cette règle alimente les optimisations en C++.

Cela dit, une fois qu'une adresse est prise d'une fonction, elle doit exister (c'est-à-dire que l'adresse doit être valide). Cela peut signifier qu'il n'est plus en ligne, mais pourrait l'être (l'optimiseur appliquera l'analyse appropriée).

Alors pourquoi un pointeur peut-il exister vers une fonction en ligne, étant donné qu'il n'y a pas d'adresse mémoire fixe des fonctions en ligne?

Non, ce n'est qu'un indice et se rapporte en grande partie à la liaison et non à l'incrustation réelle. Cela alimente, ce qui est sans doute la principale utilisation actuelle, la définition de fonctions dans les fichiers d'en-tête.

Ne doit-il pas afficher différentes valeurs d'adresse de n à chaque fois que func() est appelée?

Il se peut que n soit une variable locale, basée sur l'emplacement de la pile lors de l'exécution de la fonction. Cela dit, la fonction inline, elle concerne la liaison, l'éditeur de liens fusionnera les fonctions sur les unités de traduction.


Comme indiqué dans les commentaires ;

... que si l'exemple est changé en static int n, alors chaque appel à la fonction doit imprimer une valeur constante (dans un seul programme bien sûr) ... et c'est vrai que le code soit ou non en ligne.

C'est, encore une fois, l'effet de l'exigence de liaison sur la variable locale n.

26
Niall

Vous lisez de vieux documents. La principale raison d'utiliser inline nowdays est d'autoriser les corps de fonction dans les fichiers d'en-tête. L'utilisation du mot clé inline avec une fonction signale à l'éditeur de liens que toutes les instances de la fonction dans les unités de traduction peuvent être combinées; avoir une fonction non en ligne dans un en-tête qui est inclus à partir de plusieurs unités provoque un comportement indéfini en raison d'une violation de règle de définition unique.

C++ 17 ajoute également variables en ligne , qui ont la même propriété que la variable peut être définie dans un en-tête, et toutes les définitions sont combinées par l'éditeur de liens au lieu de provoquer une violation ODR.

Ce dont vous parlez avec "le code copié dans la fonction appelante" est appelé en ligne et est indépendant du mot clé inline. Le compilateur décidera de le faire ou non, en fonction des paramètres d'optimisation, pour les fonctions non en ligne ainsi que pour les fonctions en ligne.

14
M.M

Les fonctions intégrées ne sont pas toujours intégrées. Cela signale simplement que le programmeur souhaite que cette fonction soit intégrée. Le compilateur est autorisé à incorporer n'importe quelle fonction, sans se soucier de savoir si le mot clé inline a été utilisé ou non.

Si l'adresse de la fonction est utilisée, la fonction n'est probablement pas insérée dans l'exécutable final, au moins dans GCC:

Lorsqu'une fonction est à la fois en ligne et statique, si tous les appels à la fonction sont intégrés dans l'appelant et que l'adresse de la fonction n'est jamais utilisée, le code assembleur de la fonction n'est jamais référencé.

documentation GCC

5
Ville-Valtteri

Mis à part le point déjà dit qu'une fonction inline n'a pas besoin d'être réellement intégrée (et de nombreuses fonctions sans inlinesont intégrées par les compilateurs modernes), il est également tout à fait envisageable de inline n appel via un pointeur de fonction. Exemple:

#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;
}

Ici, foo(succ, i) pourrait dans son ensemble être aligné à seulement i+1. Et en effet, cela semble se produire: g++ -O3 -S Produit du code pour les fonctions foo et 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

Mais ensuite, il génère du code pour main qui ne fait jamais référence à l'un ou l'autre, à la place, inclut simplement un nouveau _GLOBAL__sub_I__Z3fooPFiiEi Spécialisé:

.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

Donc, dans ce cas, le programme réel ne contient même pas de pointeur de fonction pointant vers succ - le compilateur a découvert que ce pointeur ferait toujours référence à la même fonction de toute façon, et a donc pu éliminer le tout sans changer le comportement. Cela peut améliorer considérablement les performances, lorsque vous appelez souvent de petites fonctions via des pointeurs de fonction. Ce qui est une technique assez répandue dans les langages fonctionnels; les compilateurs pour des langages comme O'Caml et Haskell utilisent ce type d'optimisation.


Avertissement: mes compétences en assemblage sont presque inexistantes. Je pourrais bien parler de détritus ici.

3
leftaroundabout