Ma tentative d'initialisation de la valeur est interprétée comme une déclaration de fonction, et pourquoi A a (()); résoudre?

158

Parmi les nombreuses choses que Stack Overflow m'a apprises, il y a ce que l'on appelle «l'analyse la plus vexante», qui est classiquement illustrée avec une ligne telle que

A a(B()); //declares a function

Bien que cela, pour la plupart, semble intuitivement être la déclaration d'un objet ade type A, prenant un Bobjet temporaire comme paramètre de constructeur, c'est en fait une déclaration d'une fonction aretournant un A, prenant un pointeur vers une fonction qui retourne Bet ne prend lui-même aucun paramètre . De même la ligne

A a(); //declares a function

relève également de la même catégorie, car au lieu d'un objet, il déclare une fonction. Maintenant, dans le premier cas, la solution de contournement habituelle pour ce problème consiste à ajouter un jeu supplémentaire de crochets / parenthèses autour du B(), car le compilateur l'interprétera alors comme la déclaration d'un objet

A a((B())); //declares an object

Cependant, dans le second cas, faire de même conduit à une erreur de compilation

A a(()); //compile error

Ma question est: pourquoi? Oui, je suis très conscient que la bonne `` solution de contournement '' consiste à le changer en A a;, mais je suis curieux de savoir ce que fait le supplément ()pour le compilateur dans le premier exemple qui ne fonctionne pas lors de sa réapplication dans le deuxième exemple. La A a((B()));solution de contournement est-elle une exception spécifique écrite dans la norme?

GRB
la source
20
(B())est juste une expression C ++, rien de plus. Ce n'est pas une sorte d'exception. La seule différence que cela fait est qu'il n'y a aucun moyen de l'analyser en tant que type, et donc ce n'est pas le cas.
Pavel Minaev
12
Il convient également de noter que le deuxième cas, A a();n'est pas de la même catégorie. Pour le compilateur , il n'y a jamais de manière différente de l'analyser: un initialiseur à cet endroit ne se compose jamais de parenthèses vides, il s'agit donc toujours d'une déclaration de fonction.
Johannes Schaub - litb
11
L'excellent point de litb est subtil mais important et mérite d'être souligné - la raison pour laquelle l'ambiguïté existe dans cette déclaration 'A a (B ())' est dans l'analyse de 'B ()' -> cela peut être à la fois une expression & une déclaration et le compilateur doit "choisir" decl sur expr - donc si B () est un decl alors "a" ne peut être qu'une fonction decl (pas une variable decl). Si '()' était autorisé à être un initialiseur 'A a ()' serait ambigu - mais pas expr vs decl, mais var decl vs func decl - il n'y a pas de règle pour préférer un decl sur un autre - et donc '() 'n'est tout simplement pas autorisé comme initialiseur ici - et l'ambiguïté ne monte pas.
Faisal Vali
6
A a();n'est pas un exemple de l' analyse la plus vexante . C'est simplement une déclaration de fonction, comme c'est le cas en C.
Pete Becker
2
"la bonne 'solution de contournement' consiste à le changer en A a;" est faux. Cela ne vous donnera pas l'initialisation d'un type POD. Pour obtenir une initialisation, écrivez A a{};.
Bravo et hth. - Alf

Réponses:

70

Il n'y a pas de réponse éclairée, c'est juste parce qu'elle n'est pas définie comme syntaxe valide par le langage C ++ ... Il en est ainsi, par définition du langage.

Si vous avez une expression à l'intérieur, elle est valide. Par exemple:

 ((0));//compiles

Encore plus simple: parce que (x)c'est une expression C ++ valide, alors que ce ()n'est pas le cas.

Pour en savoir plus sur la définition des langages et le fonctionnement des compilateurs, vous devez vous renseigner sur la théorie du langage formel ou plus particulièrement sur les grammaires sans contexte (CFG) et sur le matériel connexe comme les machines à états finis. Si cela vous intéresse bien que les pages wikipedia ne suffisent pas, vous devrez vous procurer un livre.

Brian R. Bondy
la source
45
Encore plus simple: parce que (x)c'est une expression C ++ valide, alors que ce ()n'est pas le cas.
Pavel Minaev
J'ai accepté cette réponse, en plus le commentaire de Pavel à ma question initiale m'a beaucoup aidé
GRB
29

Déclarateurs de fonction C

Tout d'abord, il y a C. En C, A a()c'est la déclaration de fonction. Par exemple, putchara la déclaration suivante. Normalement, ces déclarations sont stockées dans des fichiers d'en-tête, cependant rien ne vous empêche de les écrire manuellement, si vous savez à quoi ressemble la déclaration de fonction. Les noms d'argument sont facultatifs dans les déclarations, je l'ai donc omis dans cet exemple.

int putchar(int);

Cela vous permet d'écrire le code comme celui-ci.

int puts(const char *);
int main() {
    puts("Hello, world!");
}

C vous permet également de définir des fonctions qui prennent des fonctions comme arguments, avec une belle syntaxe lisible qui ressemble à un appel de fonction (enfin, c'est lisible, tant que vous ne retournerez pas de pointeur sur la fonction).

#include <stdio.h>

int eighty_four() {
    return 84;
}

int output_result(int callback()) {
    printf("Returned: %d\n", callback());
    return 0;
}

int main() {
    return output_result(eighty_four);
}

Comme je l'ai mentionné, C permet d'omettre les noms d'argument dans les fichiers d'en-tête, donc output_resultcela ressemblerait à ceci dans le fichier d'en-tête.

int output_result(int());

Un argument dans le constructeur

Vous ne reconnaissez pas celui-là? Eh bien, laissez-moi vous le rappeler.

A a(B());

Oui, c'est exactement la même déclaration de fonction. Aest int, aest output_resultet Best int.

Vous pouvez facilement remarquer un conflit de C avec les nouvelles fonctionnalités de C ++. Pour être exact, les constructeurs étant le nom de classe et les parenthèses, et la syntaxe de déclaration alternative avec ()au lieu de =. De par sa conception, C ++ essaie d'être compatible avec le code C, et par conséquent, il doit gérer ce cas - même si pratiquement personne ne s'en soucie. Par conséquent, les anciennes fonctionnalités C ont priorité sur les nouvelles fonctionnalités C ++. La grammaire des déclarations essaie de faire correspondre le nom en tant que fonction, avant de revenir à la nouvelle syntaxe avec ()si elle échoue.

Si l'une de ces fonctionnalités n'existait pas ou avait une syntaxe différente (comme {}dans C ++ 11), ce problème ne se serait jamais produit pour la syntaxe avec un argument.

Maintenant, vous pouvez demander pourquoi A a((B()))fonctionne. Eh bien, déclarons output_resultavec des parenthèses inutiles.

int output_result((int()));

Ça ne marchera pas. La grammaire exige que la variable ne soit pas entre parenthèses.

<stdin>:1:19: error: expected declaration specifiers or ‘...’ before ‘(’ token

Cependant, C ++ attend ici une expression standard. En C ++, vous pouvez écrire le code suivant.

int value = int();

Et le code suivant.

int value = ((((int()))));

C ++ s'attend à ce que l'expression entre parenthèses soit ... eh bien ... expression, contrairement au type que C attend. Les parenthèses ne signifient rien ici. Cependant, en insérant des parenthèses inutiles, la déclaration de fonction C n'est pas mise en correspondance et la nouvelle syntaxe peut être mise en correspondance correctement (qui attend simplement une expression, telle que 2 + 2).

Plus d'arguments dans le constructeur

Un argument est certainement intéressant, mais qu'en est-il de deux? Ce n'est pas que les constructeurs peuvent avoir un seul argument. L'une des classes intégrées qui prend deux arguments eststd::string

std::string hundred_dots(100, '.');

Tout va bien (techniquement, l'analyse serait la plus vexante si elle était écrite comme std::string wat(int(), char()), mais soyons honnêtes - qui écrirait cela? Mais supposons que ce code a un problème épineux. Vous supposeriez que vous devez mettre tout entre parenthèses.

std::string hundred_dots((100, '.'));

Pas tout à fait.

<stdin>:2:36: error: invalid conversion from char to const char*’ [-fpermissive]
In file included from /usr/include/c++/4.8/string:53:0,
                 from <stdin>:1:
/usr/include/c++/4.8/bits/basic_string.tcc:212:5: error:   initializing argument 1 of std::basic_string<_CharT, _Traits, _Alloc>::basic_string(const _CharT*, const _Alloc&) [with _CharT = char; _Traits = std::char_traits<char>; _Alloc = std::allocator<char>]’ [-fpermissive]
     basic_string<_CharT, _Traits, _Alloc>::
     ^

Je ne sais pas pourquoi g ++ essaie de se convertir charen const char *. Dans tous les cas, le constructeur a été appelé avec une seule valeur de type char. Il n'y a pas de surcharge qui a un argument de type char, donc le compilateur est confus. Vous vous demandez peut-être pourquoi l'argument est de type char?

(100, '.')

Oui, ,voici un opérateur virgule. L'opérateur virgule prend deux arguments et donne l'argument de droite. Ce n'est pas vraiment utile, mais c'est quelque chose que je connais pour mon explication.

Au lieu de cela, pour résoudre l'analyse la plus vexante, le code suivant est nécessaire.

std::string hundred_dots((100), ('.'));

Les arguments sont entre parenthèses, pas l'expression entière. En fait, une seule des expressions doit être entre parenthèses, car il suffit de rompre légèrement avec la grammaire C pour utiliser la fonctionnalité C ++. Les choses nous amènent au point de zéro argument.

Zéro argument dans le constructeur

Vous avez peut-être remarqué la eighty_fourfonction dans mon explication.

int eighty_four();

Oui, cela est également affecté par l'analyse la plus vexante. C'est une définition valide, et celle que vous avez probablement vue si vous avez créé des fichiers d'en-tête (et vous devriez). L'ajout de parenthèses ne résout pas le problème.

int eighty_four(());

Pourquoi est-ce si? Eh bien, ce ()n'est pas une expression. En C ++, vous devez mettre une expression entre parenthèses. Vous ne pouvez pas écrire auto value = ()en C ++, car ()cela ne veut rien dire (et même si c'était le cas, comme un tuple vide (voir Python), ce serait un argument, pas zéro). En pratique, cela signifie que vous ne pouvez pas utiliser la syntaxe abrégée sans utiliser la syntaxe de C ++ 11 {}, car il n'y a pas d'expressions à mettre entre parenthèses et la grammaire C pour les déclarations de fonction s'appliquera toujours.

Konrad Borowski
la source
12

Vous pourriez à la place

A a(());

utilisation

A a=A();
user265149
la source
32
La «meilleure solution de contournement» n'est pas équivalente. int a = int();initialise aavec 0, int a;laisse anon initialisé. Une solution de contournement correcte consiste à utiliser A a = {};pour les agrégats, A a;lorsque l'initialisation par défaut fait ce que vous voulez, et A a = A();dans tous les autres cas, ou simplement à l'utiliser de manière A a = A();cohérente. En C ++ 11, utilisez simplementA a {};
Richard Smith
6

Les parens les plus intimes de votre exemple seraient une expression, et en C ++ la grammaire définit un expressioncomme étant un assignment-expressionou un autre expressionsuivi d'une virgule et d'un autre assignment-expression(Annexe A.4 - Récapitulatif de grammaire / Expressions).

La grammaire définit en outre un assignment-expression comme l'un des nombreux autres types d'expression, dont aucun ne peut être rien (ou seulement des espaces).

Donc, la raison pour laquelle vous ne pouvez pas avoir A a(())est simplement parce que la grammaire ne le permet pas. Cependant, je ne peux pas répondre pourquoi les personnes qui ont créé C ++ n'ont pas permis cette utilisation particulière de parens vides comme une sorte de cas spécial - je suppose qu'ils préfèrent ne pas mettre dans un cas aussi spécial s'il y avait une alternative raisonnable.

Michael Burr
la source