区分两个零参数构造函数的惯用方式


41

我有这样的课:

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    // more stuff

};

通常我想默认(零)初始化counts数组,如图所示。

但是,在通过性能分析确定的选定位置,我想抑制数组初始化,因为我知道数组即将被覆盖,但是编译器不够聪明,无法弄清楚它。

创建这样的“辅助”零参数构造函数的惯用而有效的方法是什么?

当前,我正在使用uninit_tag作为伪参数传递的标记类,如下所示:

struct uninit_tag{};

struct event_counts {
    uint64_t counts[MAX_COUNTERS];

    event_counts() : counts{} {}

    event_counts(uninit_tag) {}

    // more stuff

};

然后,像event_counts c(uninit_tag{});想抑制构造时一样,调用no-init构造函数。

我对不涉及创建哑类或以某种方式更有效的解决方案持开放态度。


“因为我知道该数组即将被覆盖”,您是否100%确定编译器尚未为您进行优化?案例:gcc.godbolt.org/z/bJnAuJ
Frank

6
@弗兰克-我觉得您的问题的答案是在您引用的句子的后半部分?它不属于问题,但可能会发生多种情况:(a)编译器通常不足以消除死存储(b)有时仅覆盖一部分元素,这不利于优化(但以后只读取相同的子集)(c)有时编译器可以做到这一点,但是失败了,例如,因为该方法未内联。
BeeOnRope

您的课程中还有其他构造函数吗?
NathanOliver

1
@Frank-嗯,您的例子说明gcc 不能消除死角吗?实际上,如果您让我猜到了,我会以为gcc可以解决这个非常简单的问题,但是如果在这里失败了,请想象任何稍微复杂的情况!
BeeOnRope

1
@uneven_mark-是的,gcc 9.2在-O3上执行了此操作(但与-O2,IME相比,这种优化并不常见),但是较早的版本却没有。通常,消除死存储是一回事,但是它非常脆弱,并且受所有常见警告的约束,例如,编译器能够在看到占主导的存储的同时看到死存储。我的评论更多是为了澄清弗兰克想说的话,因为他说的是“恰当的例子:(godbolt链接)”,但是该链接显示了两个商店都在执行(所以也许我遗漏了一些东西)。
BeeOnRope19年

Answers:


33

您已经拥有的解决方案是正确的,并且正是我要查看您的代码时想要的解决方案。它尽可能高效,清晰和简洁。


1
我的主要问题是,是否应该uninit_tag在要使用该惯用语的每个地方声明新的风味。我希望已经有类似的指标类型,也许在中std::
BeeOnRope

9
标准库中没有明显的选择。我不会为需要该功能的每个类定义一个新标签,而是定义一个项目范围的no_init标签,并在需要的所有类中使用它。
约翰·兹温克

2
我认为标准库具有用于区分迭代器和此类内容以及两者std::piecewise_construct_t和的男子汉标签std::in_place_t。在这里似乎没有一个合理的选择。也许您想定义一个类型的全局对象以始终使用,因此您不需要在每个构造函数调用中都使用大括号。STL使用std::piecewise_constructfor进行此操作std::piecewise_construct_t
n314159

它不是尽可能高效。例如,在AArch64调用约定中,标记必须进行堆栈分配,并具有连锁效应(不能尾部调用...):godbolt.org/z/6mSsmq
TLW

1
@TLW将主体添加到构造函数后,就没有堆栈分配了,godbolt.org
z/

8

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

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

然后,默认初始化 event_counts counts;将保持counts.counts未初始化状态(默认初始化在此处为no-op),而值初始化 event_counts counts{};将值为initialize counts.counts,有效地将其填充为零。


3
但是再一次,您必须记住要使用值初始化,并且OP希望默认情况下它是安全的。
doc

@doc,我同意。这不是OP想要的确切解决方案。但是此初始化模仿内置类型。因为int i;我们接受它不是零初始化的。也许我们还应该接受event_counts counts;未初始化为零的值,然后将其event_counts counts{};设为新的默认值。
EVG

6

我喜欢你的解决方案。您可能还考虑了嵌套的struct和static变量。例如:

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
}

3

我认为枚举比标记类或布尔值更好。您不需要传递一个结构实例,并且从调用方很明显您将获得哪个选项。

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);
        }
    }
};

然后创建实例如下所示:

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};

我同意你的观点:枚举更简单。但也许您忘记了这条线:event_counts() : counts{} {}
蓝蓝的

@蓝蓝,我的意图不是counts无条件地初始化,而只是在INIT设置时。
TimK

@bluish我认为选择标签类的主要原因不是实现简单性,而是要表明未初始化的对象是特殊的,即它使用优化功能而不是类接口的常规部分。这两个boolenum是体面的,但我们必须意识到,使用参数,而不是超载具有略微不同的语义阴影。在前一种中,您明确地参数化了一个对象,因此,初始化/未初始化的姿势成为其状态,而将标记对象传递给ctor更像是要求类执行转换。因此,这不是IMO的语法选择问题。
doc

@TimK但是OP希望默认行为是数组的初始化,因此我认为您对问题的解决方案应包括event_counts() : counts{} {}
蓝蓝的

@bluish在我最初的建议counts中,std::fill除非NO_INIT有要求,否则初始化为。按照您的建议添加默认构造函数将采用两种不同的方式进行默认初始化,但这不是一个好主意。我添加了另一种避免使用的方法std::fill
TimK

1

您可能要考虑对类进行两阶段初始化

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()在构造后调用成员函数。


7
谢谢,我考虑了这种方法,但是想要一种可以保持默认安全的方法-即默认情况下为零,并且仅在少数几个选定的位置,我才将行为覆盖为不安全的行为。
BeeOnRope

3
这将需要采取额外的措施,应该被初始化的用途除外。因此,它是与OPs解决方案相关的bug的额外来源。
胡桃木

@BeeOnRope还可以提供std::functionset_zero默认参数类似的构造函数参数。如果需要未初始化的数组,则可以传递一个lambda函数。
doc

1

我会这样:

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(false),并且可以准确地说出您的意思,而不是使类的界面变得如此怪异。


8
您对效率的看法是正确的,但布尔型参数对于可读的客户端代码没有帮助。当您阅读并看到声明时event_counts(false),这是什么意思?不回头看看参数名称,您一无所知。最好至少使用一个枚举,或者在这种情况下使用问题中所示的哨兵/标签类。然后,您会得到一个更像的声明event_counts(no_init),其含义对每个人都是显而易见的。
科迪·格雷

我认为这也是不错的解决方案。您可以舍弃默认ctor并使用默认值event_counts(bool initCountr = true)
doc

另外,ctor应该是明确的。
doc

不幸的是,当前C ++不支持命名参数,但是我们可以使用boost::parameter并呼吁event_counts(initCounts = false)可读性
phuclv

1
有趣的,由于每个参数都有默认值,@ doc event_counts(bool initCounts = true)实际上默认的构造函数。要求只是它可以在不指定参数的情况下被调用,event_counts ec;不关心它是否没有参数或使用默认值。
贾斯汀时间-恢复莫妮卡

1

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

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{}) {}
};

您可以通过将未初始化的构造函数的参数更改为来消除虚拟类 bool or int或其他东西,因为它不再需要助记符了。

您还可以交换继承关系,并events_count_no_init使用其答案中建议的默认构造函数(如Evg)进行定义,然后将events_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{} {}
};

这是一个有趣的想法,但是我也觉得引入一种新类型会引起摩擦。例如,当我实际上想要一个未初始化的对象时event_counts,我会希望它的类型event_count不是event_count_uninitialized,所以我应该像构造那样对它进行切片event_counts c = event_counts_no_init{};,我认为这消除了打字方面的大部分节省。
BeeOnRope19年

@BeeOnRope好吧,在大多数情况下,event_count_uninitialized对象就是event_count对象。这就是继承的全部要点,它们不是完全不同的类型。
罗斯岭

同意,但碰到“用于大多数目的”。它们不可互换-例如,如果您尝试查看分配ecuec它的工作原理,而不是周围的其他方法。或者,如果您使用模板函数,则它们的类型是不同的,并且以不同的实例化结束,即使行为最终是相同的(有时也不会是静态模板成员)。尤其是在大量使用时,auto肯定会出现混乱的情况:我不希望将对象的初始化方式永久反映在其类型中。
BeeOnRope19年
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.