C++ 区分两个零参数构造函数的惯用方法

C++ 区分两个零参数构造函数的惯用方法,c++,performance,constructor,C++,Performance,Constructor,我有一门课是这样的: struct event_counts { uint64_t counts[MAX_COUNTERS]; event_counts() : counts{} {} // more stuff }; struct event_counts { uint64_t counts[MAX_COUNTERS]; event_counts() : counts{} {} event_counts(bool initCount

我有一门课是这样的:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(bool initCounts) {
        if (initCounts) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};
event_counts e1{};
event_counts e2{event_counts::INIT};
event_counts e3{event_counts::NO_INIT};
通常我希望默认(零)初始化
计数
数组,如图所示


但是,在通过分析识别的选定位置,我想抑制数组初始化,因为我知道数组即将被覆盖,但编译器没有足够的智能来解决这个问题

创建这种“次要”零参数构造函数的惯用有效方法是什么

目前,我正在使用一个标记类
uninit\u tag
,它作为伪参数传递,如下所示:

struct uninit_tag{};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(uninit_tag) {}

    // more stuff

};
然后我调用no init构造函数,比如
event_counts c(uninit_tag{})当我想要抑制构造时


我愿意接受不涉及创建虚拟类的解决方案,或者在某种程度上更有效的解决方案,等等。

您已有的解决方案是正确的,并且正是我在查看您的代码时希望看到的。它尽可能的高效、清晰和简洁。

您可能需要考虑类的两阶段初始化:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() = default;

    void set_zero() {
       std::fill(std::begin(counts), std::end(counts), 0u);
    }
};

上面的构造函数没有将数组初始化为零。要将数组的元素设置为零,必须在构造后调用成员函数
set_zero()

我喜欢您的解决方案。您可能还考虑了嵌套结构和静态变量。例如:

struct event_counts {
    static constexpr struct uninit_tag {} uninit = uninit_tag();

    uint64_t counts[MAX_COUNTS];

    event_counts() : counts{} {}

    explicit event_counts(uninit_tag) {}

    // more stuff

};
对于静态变量,未初始化的构造函数调用似乎更方便:

event_counts e(event_counts::uninit);
当然,您可以引入一个宏来保存键入并使其更具系统性

#define UNINIT_TAG static constexpr struct uninit_tag {} uninit = uninit_tag();

struct event_counts {
    UNINIT_TAG
}

struct other_counts {
    UNINIT_TAG
}

如果构造函数主体为空,则可以省略或默认:

struct event_counts {
    std::uint64_t counts[MAX_COUNTERS];
    event_counts() = default;
};

然后默认初始化
事件计数将保留未初始化的
counts.counts
计数(此处默认初始化为no-op),值初始化
event_counts{}将值初始化
计数。计数
,有效地用零填充。

我将这样做:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(bool initCounts) {
        if (initCounts) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};
event_counts e1{};
event_counts e2{event_counts::INIT};
event_counts e3{event_counts::NO_INIT};

当您使用
事件计数(false)
时,编译器将足够聪明,可以跳过所有代码,并且您可以准确地说出您的意思,而不是让类的接口变得如此怪异。

我将使用一个子类来节省一些输入:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}
    event_counts(uninit_tag) {}
};    

struct event_counts_no_init: event_counts {
    event_counts_no_init(): event_counts(uninit_tag{}) {}
};
您可以通过将not initialized构造函数的参数更改为
bool
int
或其他内容来摆脱伪类,因为它不再需要助记符

您还可以交换继承并使用默认构造函数(如答案中建议的Evg)定义
events\u count\u no\u init
,然后让
events\u count
成为子类:

struct event_counts_no_init {
    uint64_t counts[MAX_COUNTERS];
    event_counts_no_init() = default;
};

struct event_counts: event_counts_no_init {
    event_counts(): event_counts_no_init{} {}
};

我认为枚举比标记类或布尔更好。您不需要传递结构的实例,而且调用者可以清楚地知道您得到的是哪个选项

struct event_counts {
    enum Init { INIT, NO_INIT };
    uint64_t counts[MAX_COUNTERS];

    event_counts(Init init = INIT) {
        if (init == INIT) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};
然后创建实例如下所示:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(bool initCounts) {
        if (initCounts) {
            std::fill(counts, counts + MAX_COUNTERS, 0);
        }
    }
};
event_counts e1{};
event_counts e2{event_counts::INIT};
event_counts e3{event_counts::NO_INIT};

或者,为了使其更像标记类方法,请使用单值枚举而不是标记类:

struct event_counts {
    enum NoInit { NO_INIT };
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}
    explicit event_counts(NoInit) {}
};
那么,只有两种方法可以创建实例:

event_counts e1{};
event_counts e2{event_counts::NO_INIT};

“因为我知道数组即将被覆盖”你100%确定你的编译器没有为你进行优化吗?举个例子:@Frank-我觉得你的问题的答案就在你引用的句子的后半部分?这不属于这个问题,但可能会发生多种情况:(a)编译器通常不足以消除死区存储(b)有时只覆盖元素的一个子集,这会破坏优化(但以后只读取相同的子集)(c)有时编译器可以做到,但失败了,例如。,因为这个方法不是内联的。你的类中还有其他构造函数吗?@Frank-eh,你的例子表明gcc没有消除死存储?事实上,如果你让我猜,我会认为gcc会把这个非常简单的案例做好,但如果它失败了,那么想象一下任何稍微复杂一点的案例吧@不均匀的标记-是的,gcc 9.2在-O3下进行了优化(但这种优化与-O2、IME相比并不常见),但早期版本没有。一般来说,死存储消除是一件事,但它非常脆弱,并且受到所有常见警告的约束,例如编译器能够在看到主要存储的同时看到死存储。我的评论更多的是为了澄清Frank想说什么,因为他说“案例说明:(godbolt link)”,但链接显示两个存储都在执行(因此可能我遗漏了一些东西)。谢谢,我考虑过这种方法,但希望有一些东西可以保持默认安全-即默认为零,只有在少数几个选定的地方,我会将行为重写为不安全的行为。这将需要额外的注意,但应该未初始化的使用除外。因此,相对于OPs解决方案,它是一个额外的bug源。@BeeOnRope one还可以提供
std::function
作为构造函数参数,并将类似于
set_zero
的内容作为默认参数。如果需要未初始化的数组,则需要传递一个lambda函数。我的主要问题是,是否应该在我想使用此习惯用法的每个地方声明一个新的
uninit\u标记
flavor。我希望已经有类似的指示符类型,可能在
std::
中。从标准库中没有明显的选择。我不会为我想要这个特性的每一个类定义一个新的标记——我会定义一个项目范围的
no_init
标记,并在需要的所有类中使用它。我认为标准库有许多用于区分迭代器和类似东西的标记,还有两个
std::piecutewise_construct
std::in_place_t
。在这里使用它们似乎都不合理。也许您想定义一个您的类型的全局对象来始终使用,这样您就不需要在每次构造函数调用中使用大括号。STL使用for
std::pieclewise\u construct\u
来实现这一点。它的效率不如