为什么没有默认构造函数就不能编译?

Why won't this compile without a default constructor?

提问人:Zebrafish 提问时间:12/17/2018 最后编辑:cpplearnerZebrafish 更新时间:4/27/2019 访问量:6064

问:

我可以这样做:

#include <iostream>

int counter;

int main()
{
    struct Boo
    {
        Boo(int num)
        {
            ++counter;
            if (rand() % num < 7) Boo(8);
        }
    };

    Boo(8);

    return 0;
}

这将编译良好,我的计数器结果是 21。但是,当我尝试创建传递构造函数参数而不是整数文本的对象时,我收到编译错误:Boo

#include <iostream>

int counter;

int main()
{
    struct Boo
    {
        Boo(int num)
        {
            ++counter;
            if (rand() % num < 7) Boo(num); // No default constructor 
                                            // exists for Boo
        }
    };

    Boo(8);

    return 0;
}

如何在第二个示例中调用默认构造函数,但在第一个示例中不调用?这是我在 Visual Studio 2017 上遇到的错误。

在在线C++编译器onlineGDB上,我收到错误:

error: no matching function for call to ‘main()::Boo::Boo()’
    if (rand() % num < 7) Boo(num);

                           ^
note:   candidate expects 1 argument, 0 provided
C++ 构造函数 作用域 default-constructor most-vexing-parse

评论

2赞 Zebrafish 12/17/2018
@NeilButterworth我知道最令人烦恼的解析,但它并没有让我感到震惊,因为我将局部变量传递给构造函数,并认为这并不模棱两可,因为我在传递局部变量标识符时并不认为它是模棱两可的。user10605163 的回答很有启发性,因为它解释了在消除歧义时不使用类型或非类型。我对赞成票一无所知,除了我对这两个答案都投了赞成票,因为我发现它们很有帮助。如果这是欺骗,您可以关闭它。
10赞 Nicol Bolas 12/17/2018
@NeilButterworth:“这些建议有什么问题吗?因为最令人烦恼的解析通常表现为创建临时函数和声明函数之间的竞争。相反,这被视为创建变量。相似的想法,相似的分辨率,但最终是不同的来源。
5赞 12/17/2018
@Nicol“这些提案有什么问题吗?” - 嗯,什么?哪里?
5赞 Eljay 12/17/2018
我对问题和答案投了赞成票,因为我学到了一些新东西。我知道最令人烦恼的解析,它的表现形式与我以前见过的完全不同。答案很有帮助,解决方法也很有趣。
12赞 Nicol Bolas 12/17/2018
@user202729:但事实并非如此。简单地说“最令人烦恼的解析”并不能回答这个问题。你必须解释它是如何“最令人烦恼的解析”的。这需要解释它在本例中尝试声明一个变量,但在其他情况下声明一个函数。这些是不同的事情,出于不同的原因,因此必须有不同的答案。

答:

34赞 user4290866 12/17/2018 #1

这被称为最令人烦恼的解析(Scott Meyers 在 Effective STL 中使用了该术语)。

Boo(num)不调用构造函数,也不创建临时构造函数。Clang 给出了一个很好的警告(即使有正确的名称 Wvexing-parse):

<source>:12:38: warning: parentheses were disambiguated as redundant parentheses around declaration of variable named 'num' [-Wvexing-parse]

所以编译器看到的等价于

Boo num;

这是一个可变的衰减。您声明了一个名为 num 的 Boo 变量,该变量需要默认构造函数,即使您想创建一个临时的 Boo 对象也是如此。c++ 标准要求编译器在您的案例中假定这是一个变量声明。你现在可能会说:“嘿,num 是一个 int,不要那样做。但是,该标准说

消除歧义纯粹是句法上的;也就是说,在这种语句中出现的名称的含义,除了它们是否是类型名称之外,通常不会在消歧中使用或改变。 根据需要实例化类模板,以确定限定名称是否为 type-name。 消除歧义先于分析,作为声明消除歧义的语句可能是格式不正确的声明。 如果在分析过程中,模板参数中的名称的绑定方式与试验分析期间的绑定方式不同,则程序的格式不正确。 无需诊断。 [ 注意:仅当名称在声明的前面声明时,才会发生这种情况。 — 尾注 ]

所以没有办法摆脱这种情况。

因为这不会发生,因为解析器可以确定这不是一个降级(8 不是有效的标识符名称)并调用构造函数 .Boo(8)Boo(int)

顺便一提:您可以使用括号来消除歧义:

 if (rand() % num < 7)  (Boo(num));

或者在我看来,更好的是,使用新的统一初始化语法

if (rand() % num < 7)  Boo{num};

然后编译看 这里这里.

评论

2赞 Rakete1111 12/17/2018
您引用了错误的部分,并且“要求 rcase 中的编译器假设这是一个函数声明”是不正确的,因为它不是函数而是变量。num
2赞 12/17/2018
感谢您的发现,稍后将不得不修复它。
0赞 Lightness Races in Orbit 12/17/2018
没有您描述的“对构造函数的调用”之类的东西;其意图是功能演员的话语,以创造一个临时的;这样做的语法看起来有点像“构造函数调用”,但实际上在语法(或语义上)没有这样的事情是可能的。在一些场景中,会调用构造函数,但这些场景都是在执行其他操作时由语言触发的。
1赞 12/17/2018
@LightnessRacesinOrbit正确。修复了它。感谢您抽出宝贵时间并帮助改进答案。
0赞 Joshua 12/18/2018
Boo (num) 编译有点尴尬;可能应该是一个语法错误,只允许 Boo (num)()。
88赞 user10605163 12/17/2018 #2

Clang 给出以下警告消息:

<source>:12:16: warning: parentheses were disambiguated as redundant parentheses around declaration of variable named 'num' [-Wvexing-parse]
            Boo(num); // No default constructor 
               ^~~~~

这是一个最棘手的解析问题。因为是类类型的名称而不是类型名称,所以可以是带有 的构造函数的临时类型的构造,也可以是声明符周围带有额外括号的声明(声明器可能始终具有括号)。如果两者都是有效的解释,则标准要求编译器承担声明。BoonumBoo(num);BoonumBooBoo num;num

如果它被解析为声明,则将调用默认构造函数(没有参数的构造函数),它既不是由您声明的,也不是隐式声明的(因为您声明了另一个构造函数)。因此,程序格式不正确。Boo num;

这不是 的问题,因为不能是变量的标识符 (declarator-id),因此它被解析为构造函数创建一个临时的 with as 参数的调用,从而不调用默认构造函数(未声明),而是调用您手动定义的构造函数。Boo(8);8Boo8

您可以通过使用 instead of (因为不允许在声明符周围)、将 temporary 设置为命名变量(例如 )或将其作为操作数放在另一个表达式中(例如 、 等)来消除声明中的歧义。Boo{num};Boo(num);{}Boo temp(num);(Boo(num));(void)Boo(num);

请注意,如果默认构造函数可用,则声明的格式将正确,因为它位于 的分支块作用域内,而不是函数的块作用域内,并且只会在函数的参数列表中隐藏 。ifnum

无论如何,将临时对象创建误用于本应是正常(成员)函数调用的内容似乎不是一个好主意。

这种特殊类型的最令人烦恼的解析,括号中只有一个非类型名称,只能发生,因为意图是创建一个临时的并立即丢弃它,或者如果意图是创建一个直接用作初始值设定项的临时,例如 (实际上声明函数取一个以 type 命名的参数并返回 )。Boo boo(Boo(num));boonumBooBoo

通常不打算立即丢弃临时值,并且可以使用大括号初始化或双参数(,或,但不是)来避免初始值设定项的情况。Boo boo{Boo(num)}Boo boo(Boo{num})Boo boo((Boo(num)));Boo boo(Boo((num)));

如果不是类型名称,则它不能是声明,并且不会出现任何问题。Boo

我还想强调的是,即使在类范围和构造函数定义中,也要创建一个新的临时类型。它不像人们错误地认为的那样,像通常的非静态成员函数那样使用调用方的指针调用构造函数。不可能在构造函数主体中以这种方式调用另一个构造函数。这只能在构造函数的成员初始值设定项列表中实现。Boo(8);Boothis


即使由于缺少构造函数而声明格式不正确,也会发生这种情况,因为 [stmt.ambig]/3

消除歧义纯粹是句法上的;也就是说,含义 在这样的陈述中出现的名字,超出了它们是否是 无论是否 type-names,通常不会在 澄清。

[...]

消除歧义先于分析,作为声明消除歧义的语句可能是格式不正确的声明。


在编辑中修复:我忽略了所讨论的声明与函数参数的范围不同,因此如果构造函数可用,则声明的格式正确。在任何情况下,在消除歧义时都不会考虑这一点。还扩展了一些细节。

评论

1赞 Zebrafish 12/17/2018
感谢您的回答,如果这是一种令人烦恼的解析问题,这是有道理的。我不明白的是 num 是一个局部变量,并且不明白这怎么会被误认为是一种类型,我认为这通常是当棘手的解析问题出现时。
1赞 12/17/2018
@Zebrafish 我添加了似乎是负责任的标准段落。它不会被误认为是一种类型。它被用作变量名称,唯一的混淆似乎是该名称已在具有不同类型的作用域中声明。
1赞 Zebrafish 12/17/2018
明白了。因此,它似乎允许 Boo(8) 并不是因为 8 在参数方面是明确的,而是因为如果我理解正确的话,8 不能是对象的标识符名称。
3赞 12/17/2018
是的,这是正确的。如果在另一个表达式中使用临时值,则也不存在此问题,因为这样它就不能是声明语句。你只看到这个奇怪的情况,因为你正在创建未命名的临时对象,然后立即再次丢弃它们。只是不要那样做。Boo(...)
1赞 Apollys supports Monica 12/18/2018
用 C/C++ 编码这么久,从来没有猜到是有效的语法......确实令人烦恼。int(x)=5;
1赞 sbh 1/6/2019 #3

这是叮当声警告

truct_init.cpp:11:11:错误:使用不同的类型重新定义“num”:“Boo” 与 'int'