添加 initializer_list 构造函数后静默中断构造函数调用

Silent breaking of constructor calls after adding initializer_list constructor

让我们考虑以下问题:

#include <iostream>
#include <initializer_list>

class Foo {
public:
    Foo(int) {
        std::cout << "with int\n";
    }
};

int main() {
    Foo a{10};  // new style initialization
    Foo b(20);  // old style initialization
}

在 运行 它打印:

with int
with int

一切顺利。现在由于新的要求,我添加了一个构造函数,它接受一个初始化列表。

Foo(std::initializer_list<int>) {
    std::cout << "with initializer list\n";
}

现在打印:

with initializer list
with int

所以我的旧代码 Foo a{10} 悄无声息地被破坏了。 a 应该用 int 初始化。

我了解语言语法将 {10} 视为包含一项的列表。但是我怎样才能防止旧代码的这种无声破坏?

  1. 是否有任何编译器选项会在这种情况下向我们发出警告?因为这将是特定于编译器的,所以我对 gcc 最感兴趣。我已经试过了 -Wall -Wextra.
  2. 如果没有这样的选项,那么我们是否总是需要使用旧式构造,即使用 () Foo b(20),对于其他构造函数,只有在我们真正想要的时候才使用 {}初始化列表?

我找不到这样的选项,所以显然在这种情况下,您应该为具有 initializer_list 构造函数的 类 使用括号,并为所有其他 类 使用统一初始化作为你希望。

可以在此答案和评论中找到一些有用的见解:

没有编译器警告,也永远不会有。警告代码做一些常见的事情是没有意义的,比如

std::vector vec{1};

请记住,编译器只会警告 真正 不需要的东西,比如未定义的行为。它无法知道在上面的定义中,您打算调用带有大小参数的构造函数。据它所知,您实际上想要 有一个只有一个元素的向量!它无法读懂你的想法:)

你第二个问题的答案基本是肯定的。你总是可以添加一个像 struct {} dummy; 这样的虚拟参数来避免使用带有初始化列表的构造函数,但实际上,唯一相同的解决方案只是使用圆括号而不是大括号(或者不要突然破坏接口)。

如果要更改所有使用列表初始化的代码,可以删除初始化列表构造函数,将它们更改为大括号,然后然后正确实现构造函数。我会认为这样的改变是一个破坏性的改变,并适当地处理它。另一个想法是事先提出初始化列表用例,并立即实施。

在这些情况下不可能生成任何警告,因为呈现的 选择 std::initializer_list 构造函数而不是直接匹配的行为是明确定义的并且符合标准。

此问题在 Scott Meyers Effective Modern C++ book 中有详细描述 第 7 项:

If, however, one or more constructors declare a parameter of type std::initializer_list, calls using the braced initialization syntax strongly prefer the overloads taking std::initializer_lists. Strongly. If there’s any way for compilers to construe a call using a braced initializer to be to a constructor taking a std::initializer_list, compilers will employ that interpretation.

他还介绍了这个问题的一些边缘案例,我强烈建议阅读。