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

Posted

技术标签:

【中文标题】为啥没有默认构造函数就不能编译?【英文标题】:Why won't this compile without a default constructor?为什么没有默认构造函数就不能编译? 【发布时间】:2019-05-17 07:52:57 【问题描述】:

我可以这样做:

#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

【问题讨论】:

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

Clang 给出了这个警告信息:

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

这是一个最令人头疼的解析问题。因为Boo 是类类型的名称而num 不是类型名称,所以Boo(num); 可以是Boo 类型的临时构造,numBoo 的构造函数的参数或者它可能是一个声明 Boo num;,在声明符 num 周围有额外的括号(声明符可能总是有)。如果两者都是有效的解释,标准要求编译器假设一个声明。

如果它被解析为声明,那么Boo num; 将调用默认构造函数(没有参数的构造函数),它不是由您声明或隐式声明的(因为您声明了另一个构造函数)。因此程序是非良构的。

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

您可以通过使用Boonum; 而不是Boo(num);(因为不允许在声明符周围使用)来消除声明中的歧义,方法是将临时变量设为命名变量,例如Boo temp(num);,或将其作为操作数放在另一个表达式中,例如(Boo(num));(void)Boo(num);

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

在任何情况下,将临时对象创建误用于应该是正常(成员)函数调用的东西似乎不是一个好主意。

这种特殊类型的括号中只有一个非类型名称的最麻烦的解析只会发生,因为意图是创建一个临时的并立即丢弃它,或者如果打算创建一个直接用作临时的临时初始化器,例如Boo boo(Boo(num));(实际上是声明函数boo接受一个名为num的参数,类型为Boo并返回Boo)。

通常不打算立即丢弃临时对象,并且可以使用大括号初始化或双括号(Boo booBoo(num)Boo boo(Boonum)Boo boo((Boo(num)));,但不是Boo boo(Boo((num)));)来避免初始化情况。

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

我还想强调Boo(8); 正在创建一个Boo 类型的新临时对象,即使在类作用域和构造函数定义中也是如此。正如人们可能错误地认为的那样,它不像通常的非静态成员函数那样使用调用者的this 指针调用构造函数。在构造函数体内不能以这种方式调用另一个构造函数。这只能在构造函数的成员初始化器列表中实现。


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

消歧纯粹是句法;也就是说, 在这样的陈述中出现的名字,除了它们是否 类型名称与否,通常不用于或由 消除歧义。

[...]

消歧先于解析,作为声明消歧的语句可能是格式错误的声明。


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

【讨论】:

感谢您的回答,如果这是一种令人烦恼的解析问题,那是有道理的。我没有得到的是 num 是一个局部变量,并且不明白这怎么可能被误认为是一种类型,我认为这通常是当令人烦恼的解析问题出现时。 @Zebrafish 我添加了似乎负责的标准段落。它不会被误认为是一种类型。它用作变量名,唯一的混淆似乎是该名称已在范围内以不同的类型声明。 我明白了。所以它似乎允许 Boo(8) 不是因为 8 就参数而言是明确的,但更多的是因为 8 不能是对象标识符的名称,如果我理解正确的话。 是的,这是正确的。如果在另一个表达式中使用临时 Boo(...),则该问题也不存在,因为它不能是声明语句。您只会看到这种奇怪的情况,因为您正在创建未命名的临时对象,然后立即再次丢弃它们。不要那样做。 用 C/C++ 编码这么久了,永远不会猜到 int(x)=5; 是有效的语法......确实很烦。【参考方案2】:

这被称为最令人头疼的解析(The term was used by Scott Meyers in 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,不要那样做。”但是,standard says:

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

所以没有办法了。

对于Boo(8),这不可能发生,因为解析器可以确定这不是声明(8 不是有效的标识符名称)并调用构造函数Boo(int)

顺便说一句:您可以使用括号消除歧义:

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

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

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

然后编译 see here 和 here。

【讨论】:

您引用了错误的部分并且“要求您的 rcase 中的编译器假定这是一个函数声明”是不正确的,因为 num 不是一个函数而是一个变量。 感谢您的发现,稍后会修复它。 您描述的方式中没有“对构造函数的调用”之类的东西;意图是一种功能性的表达,以创造一个临时的;执行此操作的语法看起来有点像“构造函数调用”,但实际上在语法(或语义)上没有这样的事情是可能的。在少数情况下会调用构造函数,但这些都是在您执行其他操作时由语言为您触发的全部 @LightnessRacesinOrbit 正确。修复。感谢您抽出宝贵时间帮助改进答案。 Boo (num) 编译有点尴尬;可能应该是只允许 Boo (num)() 的语法错误。【参考方案3】:

这里是铿锵警告

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

【讨论】:

以上是关于为啥没有默认构造函数就不能编译?的主要内容,如果未能解决你的问题,请参考以下文章

构造函数的特点

子类为啥要调用父类的构造函数

为啥当类包含任何参数化构造函数时编译器不提供默认构造函数? [复制]

c+学习记录

为啥 C++ 构造函数在继承中需要默认参数?

golang函数中的参数为啥不支持默认值