web-dev-qa-db-fra.com

Pourquoi ne puis-je pas définir une fonction dans une autre fonction?

Ce n'est pas une question de fonction lambda, je sais que je peux assigner un lambda à une variable.

Quel est l'intérêt de nous permettre de déclarer, mais pas de définir une fonction dans le code?

Par exemple:

#include <iostream>

int main()
{
    // This is illegal
    // int one(int bar) { return 13 + bar; }

    // This is legal, but why would I want this?
    int two(int bar);

    // This gets the job done but man it's complicated
    class three{
        int m_iBar;
    public:
        three(int bar):m_iBar(13 + bar){}
        operator int(){return m_iBar;}
    }; 

    std::cout << three(42) << '\n';
    return 0;
}

Ce que je veux savoir, c'est pourquoi le C++ autorise-t-il two qui semble inutile, et three qui semble beaucoup plus compliqué, mais interdit one?

EDIT:

D'après les réponses, il semble que la déclaration dans le code puisse empêcher la pollution des espaces de noms. J'espérais cependant comprendre pourquoi la possibilité de déclarer des fonctions a été autorisée, mais la possibilité de définir des fonctions a été interdite.

77
Jonathan Mee

La raison pour laquelle one n'est pas autorisée n'est pas évidente; des fonctions imbriquées ont été proposées il y a longtemps dans N0295 qui dit:

Nous discutons de l’introduction de fonctions imbriquées dans C++. Les fonctions imbriquées sont bien comprises et leur introduction nécessite peu d'effort de la part des vendeurs de compilateurs, des programmeurs ou du comité. Les fonctions imbriquées offrent des avantages significatifs, [...]

De toute évidence, cette proposition a été rejetée, mais comme nous n'avons pas de compte rendu de réunion disponible en ligne pour 1993 nous n’avons pas de source possible pour justifier ce rejet.

En fait, cette proposition est notée dans expressions lambda et fermetures pour C++ comme alternative possible:

Un article [Bre88] et la proposition N0295 au comité C++ [SH93] suggèrent d'ajouter des fonctions imbriquées au C++. Les fonctions imbriquées sont similaires aux expressions lambda, mais sont définies comme des instructions dans un corps de fonction et la fermeture résultante ne peut être utilisée que si cette fonction est active. Ces propositions n'incluent pas non plus l'ajout d'un nouveau type pour chaque expression lambda, mais leur implémentation plutôt comme des fonctions normales, permettant notamment à un type spécial de pointeur de fonction de s'y référer. Ces deux propositions sont antérieures à l'ajout de modèles à C++ et ne mentionnent donc pas l'utilisation de fonctions imbriquées en combinaison avec des algorithmes génériques. En outre, ces propositions ne permettent pas de copier des variables locales dans une fermeture. Les fonctions imbriquées qu'elles produisent sont donc complètement inutilisables en dehors de leur fonction englobante.

Étant donné que nous avons maintenant des lambdas, il est peu probable que nous voyions des fonctions imbriquées car, comme le décrit le document, elles constituent une alternative au même problème et les fonctions imbriquées présentent plusieurs limitations par rapport à lambdas.

En ce qui concerne cette partie de votre question:

// This is legal, but why would I want this?
int two(int bar);

Il y a des cas où ce serait un moyen utile d'appeler la fonction souhaitée. Le projet de section standard C++ 3.4.1 [basic.lookup.unqual] nous en donne un exemple intéressant:

namespace NS {
    class T { };
    void f(T);
    void g(T, int);
}

NS::T parm;
void g(NS::T, float);

int main() {
    f(parm); // OK: calls NS::f
    extern void g(NS::T, float);
    g(parm, 1); // OK: calls g(NS::T, float)
}
40
Shafik Yaghmour

Eh bien, la réponse est "des raisons historiques". En C, vous pourriez avoir des déclarations de fonction à la portée d'un bloc, et les concepteurs C++ ne voyaient pas l'intérêt de supprimer cette option.

Un exemple d'utilisation serait:

#include <iostream>

int main()
{
    int func();
    func();
}

int func()
{
    std::cout << "Hello\n";
}

OMI c'est une mauvaise idée car il est facile de se tromper en fournissant une déclaration qui ne correspond pas à la définition réelle de la fonction, ce qui conduit à un comportement indéfini qui ne sera pas diagnostiqué par le compilateur.

31
M.M

Dans l'exemple que vous donnez, void two(int) est déclaré en tant que fonction externe, avec cette déclaration étant uniquement valable dans le cadre de la fonction main,.

C'est raisonnable si vous souhaitez uniquement rendre le nom two disponible dans main() afin d'éviter de polluer l'espace de nom global au sein de l'unité de compilation actuelle.

Exemple en réponse aux commentaires:

main.cpp:

int main() {
  int foo();
  return foo();
}

foo.cpp:

int foo() {
  return 0;
}

pas besoin de fichiers d'en-tête. compiler et relier avec

c++ main.cpp foo.cpp 

il sera compilé et exécuté, et le programme retournera 0 comme prévu.

23
Richard Hodges

Vous pouvez faire ces choses, en grande partie parce qu’elles ne sont pas si difficiles à faire.

Du point de vue du compilateur, avoir une déclaration de fonction dans une autre fonction est assez simple à implémenter. Le compilateur a besoin d'un mécanisme permettant aux déclarations à l'intérieur des fonctions de gérer d'autres déclarations (par exemple, int x;) Dans une fonction de toute façon.

Il aura généralement un mécanisme général pour analyser une déclaration. Pour le gars qui écrit le compilateur, peu importe que ce mécanisme soit invoqué pour analyser le code à l'intérieur ou à l'extérieur d'une autre fonction - c'est juste une déclaration, donc quand il en voit assez pour savoir qu'il y a une déclaration, il appelle la partie du compilateur qui traite les déclarations.

En fait, interdire ces déclarations particulières à l'intérieur d'une fonction ajouterait probablement une complexité supplémentaire, car le compilateur aurait alors besoin d'une vérification entièrement gratuite pour voir s'il examine déjà le code dans une définition de fonction et sur la base de cette décision pour autoriser ou interdire ce type de fonction. déclaration.

Cela laisse la question de savoir comment une fonction imbriquée est différente. Une fonction imbriquée est différente en raison de son incidence sur la génération de code. Dans les langages qui autorisent les fonctions imbriquées (par exemple, Pascal), vous vous attendez normalement à ce que le code de la fonction imbriquée ait un accès direct aux variables de la fonction dans laquelle il est imbriqué. Par exemple:

int foo() { 
    int x;

    int bar() { 
        x = 1; // Should assign to the `x` defined in `foo`.
    }
}

Sans fonctions locales, le code d'accès aux variables locales est assez simple. Dans une implémentation typique, lorsque l'exécution entre dans la fonction, un bloc d'espace pour les variables locales est alloué sur la pile. Toutes les variables locales sont allouées dans ce bloc unique et chaque variable est traitée comme un simple décalage par rapport au début (ou à la fin) du bloc. Par exemple, considérons une fonction comme celle-ci:

int f() { 
   int x;
   int y;
   x = 1;
   y = x;
   return y;
}

Un compilateur (en supposant qu'il n'optimise pas le code supplémentaire) pourrait générer un code équivalent à ceci:

stack_pointer -= 2 * sizeof(int);      // allocate space for local variables
x_offset = 0;
y_offset = sizeof(int);

stack_pointer[x_offset] = 1;                           // x = 1;
stack_pointer[y_offset] = stack_pointer[x_offset];     // y = x;
return_location = stack_pointer[y_offset];             // return y;
stack_pointer += 2 * sizeof(int);

En particulier, il possède un emplacement pointant vers le début du bloc de variables locales, et tous les accès aux variables locales sont décalés par rapport à cet emplacement.

Avec les fonctions imbriquées, ce n'est plus le cas. Au lieu de cela, une fonction a accès non seulement à ses propres variables locales, mais également aux variables locales de toutes les fonctions dans lesquelles elle est imbriquée. Au lieu d’avoir un seul "stack_pointer" à partir duquel il calcule un décalage, il doit remonter dans la pile pour trouver les stack_pointers locaux aux fonctions dans lesquelles il est imbriqué.

Maintenant, dans un cas trivial, ce n'est pas si terrible non plus - si bar est imbriqué à l'intérieur de foo, alors bar peut simplement rechercher la pile dans le pointeur de pile précédent accéder aux variables de foo. Droite?

Faux! Bien, il y a des cas où cela peut être vrai, mais ce n'est pas nécessairement le cas. En particulier, bar pourrait être récursif, auquel cas un appel donné de bar pourrait devoir rechercher un nombre presque arbitraire de niveaux sauvegardant la pile pour rechercher les variables de la fonction environnante. En règle générale, vous devez effectuer l'une des deux opérations suivantes: soit vous ajoutez des données supplémentaires à la pile, de sorte qu'il puisse effectuer une recherche dans la pile lors de l'exécution pour trouver le cadre de pile de sa fonction environnante, ou bien vous passez un pointeur sur le cadre de pile de la fonction environnante en tant que paramètre masqué de la fonction imbriquée. Oh, mais il n'y a pas nécessairement non plus une fonction environnante non plus - si vous pouvez imbriquer des fonctions, vous pouvez probablement les imbriquer (plus ou moins) de manière arbitraire, vous devez donc être prêt à transmettre un nombre arbitraire de paramètres cachés. Cela signifie que vous obtenez généralement quelque chose comme une liste liée de cadres de pile aux fonctions environnantes, et l'accès aux variables des fonctions environnantes est effectué en parcourant cette liste liée pour trouver son pointeur de pile, puis en accédant à un décalage à partir de ce pointeur de pile.

Cela signifie toutefois que l'accès à une variable "locale" peut ne pas être une mince affaire. La recherche du cadre de pile correct pour accéder à la variable peut être non triviale, de sorte que l'accès aux variables des fonctions environnantes est également (du moins généralement) plus lent que l'accès aux variables réellement locales. Et, bien sûr, le compilateur doit générer du code pour trouver les bons cadres de pile, accéder aux variables via un nombre quelconque de cadres de pile, etc.

Ceci est la complexité que C évitait en interdisant les fonctions imbriquées. Maintenant, il est certainement vrai qu'un compilateur C++ actuel est une sorte de bête assez différente d'un compilateur C vintage des années 1970. Avec des éléments tels que l'héritage virtuel multiple, un compilateur C++ doit dans tous les cas traiter des éléments de la même nature générale (c'est-à-dire que trouver l'emplacement d'une variable de classe de base peut également être non trivial). En pourcentage, la prise en charge de fonctions imbriquées n’ajouterait pas beaucoup de complexité au compilateur C++ actuel (et certaines, telles que gcc, les prennent déjà en charge).

En même temps, cela ajoute rarement beaucoup d’utilité. En particulier, si vous voulez définir quelque chose qui agit comme une fonction à l'intérieur d'une fonction, vous pouvez utiliser une expression lambda. Ce que cela crée réellement est un objet (c'est-à-dire une instance d'une classe) qui surcharge l'opérateur d'appel de fonction (operator()), mais il offre toujours des fonctionnalités similaires à celles d'une fonction. Cela rend toutefois plus explicite la saisie (ou non) des données du contexte environnant, ce qui lui permet d’utiliser les mécanismes existants au lieu d’inventer un tout nouveau mécanisme et un ensemble de règles pour son utilisation.

Conclusion: bien qu’il puisse sembler au départ que les déclarations imbriquées sont dures et que les fonctions imbriquées soient triviales, l’inverse est plus ou moins vrai: les fonctions imbriquées sont en réalité beaucoup plus complexes à prendre en charge que les déclarations imbriquées.

18
Jerry Coffin

Le premier est une définition de fonction, et ce n'est pas autorisé. Il est évident que wt est l’utilisation de la définition d’une fonction dans une autre fonction.

Mais les deux autres ne sont que des déclarations. Imaginez que vous deviez utiliser la fonction int two(int bar); dans la méthode principale. Mais il est défini sous la fonction main(), de sorte que la déclaration de la fonction à l'intérieur de la fonction vous oblige à utiliser cette fonction avec des déclarations.

La même chose s'applique à la troisième. Les déclarations de classe à l'intérieur de la fonction vous permettent d'utiliser une classe à l'intérieur de la fonction sans fournir d'en-tête ou de référence approprié.

int main()
{
    // This is legal, but why would I want this?
    int two(int bar);

    //Call two
    int x = two(7);

    class three {
        int m_iBar;
        public:
            three(int bar):m_iBar(13 + bar) {}
            operator int() {return m_iBar;}
    };

    //Use class
    three *threeObj = new three();

    return 0;
}
5
ANjaNA

Cette fonctionnalité de langage a été héritée de C, où elle avait une certaine utilité à ses débuts (déclaration de la fonction peut-être?). Je ne sais pas si cette fonctionnalité est beaucoup utilisée par les programmeurs C modernes et j'en doute sincèrement.

Donc, pour résumer la réponse:

cette fonctionnalité n'a aucun intérêt pour moderne C++ (à ma connaissance, du moins), elle est ici en raison de la compatibilité ascendante C++-à-C (je suppose :)).


Merci au commentaire ci-dessous:

Le prototype de fonction est limité à la fonction dans laquelle il est déclaré. Vous pouvez donc avoir un espace de noms global plus ordonné - en faisant référence à des fonctions/symboles externes sans #include.

4
mr.pd

En fait, il y a un cas d'utilisation qui pourrait être utile. Si vous voulez vous assurer qu'une certaine fonction est appelée (et que votre code soit compilé), peu importe ce que le code environnant déclare, vous pouvez ouvrir votre propre bloc et y déclarer le prototype de la fonction. (L’inspiration vient de Johannes Schaub, https://stackoverflow.com/a/929902/3150802 , via TeKa, https://stackoverflow.com/a/8821992/3150802 ).

Cela peut être particulièrement utile si vous devez inclure des en-têtes que vous ne contrôlez pas ou si vous avez une macro multiligne pouvant être utilisée dans un code inconnu.

La clé est qu'une déclaration locale remplace les déclarations précédentes dans le bloc le plus intérieur. Bien que cela puisse introduire des bugs subtils (et, je pense, est interdit en C #), il peut être utilisé consciemment. Considérer:

// somebody's header
void f();

// your code
{   int i;
    int f(); // your different f()!
    i = f();
    // ...
}

La liaison peut être intéressante car les en-têtes appartiennent probablement à une bibliothèque, mais je suppose que vous pouvez ajuster les arguments de l'éditeur de liens afin que f() soit résolu en fonction au moment où cette bibliothèque est considérée. Ou vous lui dites d'ignorer les symboles en double. Ou vous ne liez pas contre la bibliothèque.

4
Peter A. Schneider

Ce n'est pas une réponse à la question du PO, mais plutôt une réponse à plusieurs commentaires.

Je suis en désaccord avec ces points dans les commentaires et les réponses: 1 les déclarations imbriquées sont prétendument inoffensives et 2 que les définitions imbriquées sont inutiles.

1 Le principal contre-exemple de l'innocuité alléguée des déclarations de fonctions imbriquées est le tristement célèbre Most Vexing Parse . À l’OMI, la confusion qui en résulte est suffisante pour justifier une règle supplémentaire interdisant les déclarations imbriquées.

2 Le premier contre-exemple à la prétendue inutilité des définitions de fonctions imbriquées est la nécessité fréquente d'exécuter la même opération à plusieurs endroits dans exactement une fonction. Il existe une solution évidente à cela:

private:
inline void bar(int abc)
{
    // Do the repeating operation
}

public: 
void foo()
{
    int a, b, c;
    bar(a);
    bar(b);
    bar(c);
}

Cependant, cette solution contamine assez souvent la définition de classe avec de nombreuses fonctions privées, chacune d'entre elles étant utilisée dans exactement un appelant. Une déclaration de fonction imbriquée serait beaucoup plus propre.

3
Michael

Répondre spécifiquement à cette question:

D'après les réponses, il semble que la déclaration dans le code puisse empêcher la pollution des espaces de noms. J'espérais cependant comprendre pourquoi la possibilité de déclarer des fonctions a été autorisée, mais la possibilité de définir des fonctions a été interdite.

Parce que considérons ce code:

int main()
{
  int foo() {

    // Do something
    return 0;
  }
  return 0;
}

Questions pour les concepteurs de langues:

  1. foo() devrait-il être disponible pour d'autres fonctions?
  2. Si oui, quel devrait être son nom? int main(void)::foo()?
  3. (Notez que 2 ne serait pas possible en C, l'initiateur de C++)
  4. Si nous voulons une fonction locale, nous avons déjà un moyen - en faire un membre statique d'une classe définie localement. Devrions-nous donc ajouter une autre méthode syntaxique pour obtenir le même résultat? Pourquoi faire ça? Cela n'augmenterait-il pas la charge de maintenance des développeurs de compilateurs C++?
  5. Etc...
2
Richard Hodges

Je voulais juste souligner que le compilateur GCC vous permet de déclarer des fonctions dans des fonctions. En savoir plus à ce sujet ici . De plus, avec l'introduction de lambdas en C++, cette question est un peu obsolète maintenant.


La possibilité de déclarer des en-têtes de fonction dans d’autres fonctions, j’ai trouvé utile dans le cas suivant:

void do_something(int&);

int main() {
    int my_number = 10 * 10 * 10;
    do_something(my_number);

    return 0;
}

void do_something(int& num) {
    void do_something_helper(int&); // declare helper here
    do_something_helper(num);

    // Do something else
}

void do_something_helper(int& num) {
    num += std::abs(num - 1337);
}

Qu'avons-nous ici? En gros, vous avez une fonction qui est supposée être appelée à partir de main, vous devez donc la déclarer comme d'habitude. Mais ensuite, vous réalisez que cette fonction a également besoin d’une autre fonction pour l’aider dans ses tâches. Ainsi, plutôt que de déclarer cette fonction d'assistance au-dessus de main, vous la déclarez à l'intérieur de la fonction qui en a besoin et vous pouvez ensuite l'appeler uniquement à partir de cette fonction.

Mon argument est que déclarer des en-têtes de fonction dans des fonctions peut être une méthode indirecte d’encapsulation de fonction, ce qui permet à une fonction de masquer certaines parties de son travail en déléguant à une autre fonction dont il seul est conscient, presque en donnant un illusion d'une fonction imbriquée.

1
smac89

Les déclarations de fonctions imbriquées sont probablement autorisées pour 1. Renvoyer des références 2. Être capable de déclarer un pointeur sur une ou plusieurs fonctions et de transmettre d'autres fonctions dans une portée limitée.

Les définitions de fonctions imbriquées ne sont probablement pas autorisées pour des raisons telles que 1. Optimisation 2. Récursivité (fonctions définies englobantes et imbriquées) 3. Ré-entrance 4. Problèmes d'accès simultané et multithread.

De ma compréhension limitée :)

0
NeutronStar