【问题标题】:Is it possible to prevent omission of aggregate initialization members?是否可以防止遗漏聚合初始化成员?
【发布时间】:2020-05-25 19:00:28
【问题描述】:

我有一个包含许多相同类型成员的结构,就像这样

struct VariablePointers {
   VariablePtr active;
   VariablePtr wasactive;
   VariablePtr filename;
};

问题是如果我忘记初始化结构成员之一(例如wasactive),像这样:

VariablePointers{activePtr, filename}

编译器不会抱怨它,但我会得到一个部分初始化的对象。我怎样才能防止这种错误?我可以添加一个构造函数,但它会重复两次变量列表,所以我必须输入三次!

如果有 C++11 的解决方案(目前我仅限于该版本),请添加 C++11 答案。不过,也欢迎更新的语言标准!

【问题讨论】:

  • 键入构造函数听起来并不那么糟糕。除非你有太多的成员,在这种情况下,也许重构是合适的。
  • @Someprogrammerdude 我认为他的意思是错误是您可能不小心忽略了初始化值
  • @theWiseBro 如果您知道数组/向量如何帮助您,您应该发布答案。不是很明显,我没看到
  • @Someprogrammerdude 但它甚至是一个警告吗?用VS2019看不到。
  • 有一个-Wmissing-field-initializers 编译标志。

标签: c++ aggregate-initialization


【解决方案1】:

我想这不是一个优雅而方便的解决方案......但也应该适用于 C++11 并给出编译时(而不是链接时)错误。

这个想法是在你的结构中添加一个额外的成员,在最后一个位置,一个没有默认初始化的类型(并且不能用 VariablePtr 类型的值初始化(或者任何前面的值的类型)

举例

struct bar
 {
   bar () = delete;

   template <typename T> 
   bar (T const &) = delete;

   bar (int) 
    { }
 };

struct foo
 {
   char a;
   char b;
   char c;

   bar sentinel;
 };

这样你就不得不在你的聚合初始化列表中添加所有元素,包括显式初始化最后一个值的值(sentinel 的整数,在示例中)或者你得到一个“调用已删除的构造函数'bar'”错误。

所以

foo f1 {'a', 'b', 'c', 1};

编译和

foo f2 {'a', 'b'};  // ERROR

没有。

不幸的是

foo f3 {'a', 'b', 'c'};  // ERROR

无法编译。

-- 编辑--

正如 MSalters 所指出的(谢谢),在我的原始示例中存在一个缺陷(另一个缺陷):bar 值可以用char 值初始化(可转换为int),所以可以使用初始化后

foo f4 {'a', 'b', 'c', 'd'};

这可能会让人非常困惑。

为了避免这个问题,我添加了以下已删除的模板构造函数

 template <typename T> 
 bar (T const &) = delete;

所以前面的f4 声明给出了编译错误,因为d 值被删除的模板构造函数截获

【讨论】:

  • 谢谢,这很好!正如您提到的那样,它并不完美,并且也使foo f; 无法编译,但也许这更像是一个功能而不是这个技巧的缺陷。如果没有比这更好的建议,我会接受。
  • 为了便于阅读,我会让 bar 构造函数接受一个名为 init_list_end 的 const 嵌套类成员
  • @GonenI - 为了便于阅读,您可以接受enum,并命名init_list_end(或简单地list_end)该enum 的值;但是可读性增加了很多打字,所以,鉴于附加值是这个答案的弱点,我不知道这是否是个好主意。
  • 可能在bar 的标题中添加类似constexpr static int eol = 0; 的内容。 test{a, b, c, eol} 对我来说似乎很容易阅读。
  • @n314159 - 好吧...变成bar::eol;它几乎与传递 enum 值一样;但我认为这并不重要:答案的核心是“在你的结构中添加一个额外的成员,在最后一个位置,没有默认初始化的类型”; bar 部分只是显示解决方案有效的一个简单示例;确切的“没有默认初始化的类型”应该取决于情况(恕我直言)。
【解决方案2】:

最简单的方法是不给成员的类型一个无参数的构造函数:

struct B
{
    B(int x) {}
};
struct A
{
    B a;
    B b;
    B c;
};

int main() {

        // A a1{ 1, 2 }; // will not compile 
        A a1{ 1, 2, 3 }; // will compile 

另一种选择:如果您的成员是 const & ,则必须初始化所有成员:

struct A {    const int& x;    const int& y;    const int& z; };

int main() {

//A a1{ 1,2 };  // will not compile 
A a2{ 1,2, 3 }; // compiles OK

如果您可以使用一个虚拟的 const & 成员,则可以将其与 @max66 的哨兵想法结合起来。

struct end_of_init_list {};

struct A {
    int x;
    int y;
    int z;
    const end_of_init_list& dummy;
};

    int main() {

    //A a1{ 1,2 };  // will not compile
    //A a2{ 1,2, 3 }; // will not compile
    A a3{ 1,2, 3,end_of_init_list() }; // will compile

来自 cppreference https://en.cppreference.com/w/cpp/language/aggregate_initialization

如果初始化子句的数量小于 成员或初始化列表完全为空,其余成员 是值初始化的。如果引用类型的成员是其中之一 剩下的成员,程序是错误的。

另一种选择是采用 max66 的哨兵思想并添加一些语法糖以提高可读性

struct init_list_guard
{
    struct ender {

    } static const end;
    init_list_guard() = delete;

    init_list_guard(ender e){ }
};

struct A
{
    char a;
    char b;
    char c;

    init_list_guard guard;
};

int main() {
   // A a1{ 1, 2 }; // will not compile 
   // A a2{ 1, init_list_guard::end }; // will not compile 
   A a3{ 1,2,3,init_list_guard::end }; // compiles OK

【讨论】:

  • 不幸的是,这使得A 无法移动并改变了复制语义(A 不再是值的集合,可以这么说):(
  • @JohannesSchaub-litb 好的。我编辑的答案中的这个想法怎么样?
  • @JohannesSchaub-litb:同样重要的是,第一个版本通过使成员指针增加了一个间接级别。更重要的是,它们必须是 的引用,并且 1,2,3 对象实际上是自动存储中的本地对象,当函数结束时超出范围。在具有 64 位指针(如 x86-64)的系统上,它使 sizeof(A) 变为 24 而不是 3。
  • 一个虚拟引用将大小从 3 个字节增加到 16 个字节(用于对齐指针(引用)成员 + 指针本身的填充。)只要你从不使用引用,它可能没问题指向超出范围的对象。我当然会担心它不会优化,并且复制它肯定不会。 (一个空的类除了它的大小之外有更好的优化机会,所以这里的第三个选项是最不坏的,但它仍然在每个对象中花费空间,至少在某些 ABI 中。我仍然担心填充伤害在某些情况下进行优化。)
【解决方案3】:

如果缺少所需的初始化程序,这是一个触发链接器错误的技巧:

struct init_required_t {
    template <class T>
    operator T() const; // Left undefined
} static const init_required;

用法:

struct Foo {
    int bar = init_required;
};

int main() {
    Foo f;
}

结果:

/tmp/ccxwN7Pn.o: In function `Foo::Foo()':
prog.cc:(.text._ZN3FooC2Ev[_ZN3FooC5Ev]+0x12): undefined reference to `init_required_t::operator int<int>() const'
collect2: error: ld returned 1 exit status

注意事项:

  • 在 C++14 之前,这完全可以防止 Foo 成为聚合。
  • 这在技术上依赖于未定义的行为(违反 ODR),但应该适用于任何健全的平台。

【讨论】:

  • 你可以删除转换运算符,然后是编译器错误。
  • @jrok 是的,但只要声明了Foo,它就是一个,即使您从未真正调用过操作员。
  • @jrok 但是即使提供了初始化它也不会编译。 godbolt.org/z/yHZNq_ 附录: 对于 MSVC,它按照您的描述工作:godbolt.org/z/uQSvDa 这是一个错误吗?
  • 不幸的是,这个技巧不适用于 C++11,因为它将成为非聚合 :( 我删除了 C++11 标签,所以你的答案也是可行的(请不要删除它),但如果可能的话,仍然首选 C++11 解决方案。
  • @Quentin:我建议在答案中列出缺点,以便未来读者考虑使用它。它看起来是侵入性最小的(编译器选项或静态分析除外),并且可能最不可能损害优化(至少不改变大小)!但是对于某些用例来说,制作非聚合可能不值得,应该在答案中列出。
【解决方案4】:

对于CppCoreCheck,有一条规则可以准确检查,如果所有成员都已初始化并且可以从警告变为错误 - 当然这通常是程序范围的。

更新:

您要检查的规则是类型安全Type.6 的一部分:

Type.6:总是初始化一个成员变量:总是初始化, 可能使用默认构造函数或默认成员初始化器。

【讨论】:

    【解决方案5】:

    对于 clang 和 gcc,您可以使用 -Werror=missing-field-initializers 进行编译,这会将缺少字段初始值设定项的警告变为错误。 godbolt

    编辑: 对于 MSVC,即使在级别 /Wall 上似乎也没有发出警告,所以我认为使用此编译器无法警告缺少初始化程序。 godbolt

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2016-06-23
      • 1970-01-01
      • 2020-05-15
      • 1970-01-01
      相关资源
      最近更新 更多