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使用forstd::pieclewise\u construct\u
来实现这一点。它的效率不如