使用一个函数调用C ++初始化多个常量类成员


50

如果我有两个不同的常量成员变量,都需要基于相同的函数调用进行初始化,是否有一种方法可以在不两次调用函数的情况下进行?

例如,分数类,其中分子和分母是常数。

int gcd(int a, int b); // Greatest Common Divisor
class Fraction {
public:
    // Lets say we want to initialize to a reduced fraction
    Fraction(int a, int b) : numerator(a/gcd(a,b)), denominator(b/gcd(a,b))
    {

    }
private:
    const int numerator, denominator;
};

由于两次调用GCD函数,这会浪费时间。您还可以定义一个新的类成员,gcd_a_b然后首先将gcd的输出分配给初始化程序列表中的输出,但这将导致内存浪费。

通常,有没有一种方法可以避免浪费的函数调用或内存?您能否在初始化列表中创建临时变量?谢谢。


5
您是否有证据表明“ GCD函数被调用了两次”?提到过两次,但这与发出两次调用它的代码的编译器不同。编译器可能会推断它是纯函数,并在第二次提及时重用其值。
埃里克塔

6
@EricTowers:是的,在某些情况下,编译器有时可以在实践中解决该问题。但前提是他们可以看到定义(或对象中的某些注释),否则无法证明它是纯净的。您应该在启用链接时优化的情况下进行编译,但并非所有人都可以。该函数可能在库中。或认为这是一个功能的情况下确实有副作用,并调用它恰好一次是正确的事情?
Peter Cordes

@EricTowers有趣的一点。实际上,我确实尝试通过在GCD函数中放置一个print语句来检查它,但是现在我意识到这将阻止它成为纯函数。
Qq0

@ Qq0:您可以通过查看编译器生成的asm进行检查,例如,将Godbolt编译器资源管理器与gcc或clang一起使用-O3。但是对于任何简单的测试实现,它实际上都可以内联函数调用。如果__attribute__((const))在原型上使用或pure而不提供可见的定义,则应让GCC或clang在两个调用之间使用相同的arg进行公共子表达式消除(CSE)。请注意,Drew的答案甚至适用于非纯函数,因此它要好得多,您应该在函数无法内联时使用它。
Peter Cordes

通常,最好避免使用非静态const成员变量。const并不经常应用的少数几个领域之一。例如,您不能分配类对象。您可以将emplace_back转换为向量,但前提是容量限制不会影响调整大小。
doug

Answers:


67

通常,有没有一种方法可以避免浪费的函数调用或内存?

是。这可以使用C ++ 11中引入的委托构造函数来完成。

委托构造函数是在初始化任何成员变量之前获取构造所需的临时值的非常有效的方法。

int gcd(int a, int b); // Greatest Common Divisor
class Fraction {
public:
    // Call gcd ONCE, and forward the result to another constructor.
    Fraction(int a, int b) : Fraction(a,b,gcd(a,b))
    {
    }
private:
    // This constructor is private, as it is an
    // implementation detail and not part of the public interface.
    Fraction(int a, int b, int g_c_d) : numerator(a/g_c_d), denominator(b/g_c_d)
    {
    }
    const int numerator, denominator;
};

出于兴趣,调用另一个构造函数产生的开销会很大吗?
Qq0

1
@ Qq0 您可以在此处观察到启用适度优化不会产生任何开销。
Drew Dormann

2
@ Qq0:C ++是围绕现代优化编译器设计的。他们可以轻松地内联此委托,尤其是即使您在类定义中(在中.h)将其可见,即使对于内联而言,实际的构造函数定义也不可见。也就是说,该gcd()调用将内联到每个构造函数调用站点中,而仅call将3操作数私有构造函数留给。
Peter Cordes

10

成员var由它们在类decleration中声明的顺序初始化,因此您可以执行以下操作(数学上)

#include <iostream>
int gcd(int a, int b){return 2;}; // Greatest Common Divisor of (4, 6) just to test
class Fraction {
public:
    // Lets say we want to initialize to a reduced fraction
    Fraction(int a, int b) : numerator{a/gcd(a,b)}, denominator(b/(a/numerator))
    {

    }
//private:
    const int numerator, denominator;//make sure that they are in this order
};
//Test
int main(){
    Fraction f{4,6};
    std::cout << f.numerator << " / " << f.denominator;
}

无需调用其他构造函数,甚至无需创建它们。


6
好的,它特别适用于GCD,但许多其他用例可能无法从args和第一个派生第二个const。并且按照本文所述,这有一个额外的划分,这与编译器可能不会进行优化的理想之选相比是另一缺点。GCD可能只花费一个部门,所以这可能和两次调用GCD一样糟糕。(假设分工在其他操作的成本中占主导地位,就像现代CPU上经常这样做一样。)
Peter Cordes

@PeterCordes,但另一个解决方案有一个额外的函数调用,并分配了更多的指令存储器。
asmmo

1
您是在谈论Drew的委派构造函数吗?显然,这可以使Fraction(a,b,gcd(a,b))委托插入呼叫者,从而降低总成本。对于编译器而言,内联比撤销此操作中的多余划分更容易。我没有在godbolt.org上尝试过,但是如果您好奇的话可以。使用gcc或clang -O3就像正常构建一样。(C ++是围绕现代优化编译器的假设而设计的,因此具有类似的功能constexpr
Peter Cordes

-3

@Drew Dormann提供了与我所想类似的解决方案。由于OP从不提及无法修改ctor,因此可以使用以下命令进行调用Fraction f {a, b, gcd(a, b)}

Fraction(int a, int b, int tmp): numerator {a/tmp}, denominator {b/tmp}
{
}

只有这样,才能再次调用函数,构造函数或其他方法,因此不会浪费时间。而且这不会浪费内存,因为无论如何都必须创建一个临时文件,因此您最好充分利用它。这也避免了额外的划分。


3
您所做的编辑甚至无法回答问题。现在,您需要调用方传递第三个参数?您在构造函数主体中使用赋值的原始版本不适用于const,但至少适用于其他类型。您还“避免”额外分裂什么?你是说对阿斯莫的答案?
Peter Cordes

1
好的,既然您已经解释了我的观点,请删除我的下一篇投票。但这似乎显然很可怕,并且需要您手动将某些构造函数内联到每个调用程序中。这与DRY(不要重复自己)和封装类的职责/内部结构相反。大多数人不会认为这是可以接受的解决方案。鉴于有C ++ 11的方法可以干净地执行此操作,因此除非有人坚持使用较旧的C ++版本,并且该类很少调用此构造函数,否则任何人都不应这样做。
Peter Cordes

2
@aconcernedcitizen:我不是出于性能原因,而是出于代码质量原因。用自己的方式,如果您曾经更改过此类的内部工作方式,则必须查找对构造函数的所有调用并更改该第3个arg。这额外,gcd(foo, bar)的额外的代码可能,因此应每调用点的分解出来。那是可维护性/可读性问题,而不是性能。编译器很可能会在编译时内联它,以提高性能。
Peter Cordes

1
@PeterCordes您说得对,现在我看到我的想法已经定在解决方案上,而我无视了其他一切。无论哪种方式,答案都是存在的,即使只是为了羞辱。每当我对此有疑问时,我都会知道在哪里寻找。
有关的公民

1
还请考虑“ Fraction f( x+y, a+b ); 以自己的方式编写” 的情况,您必须编写BadFraction f( x+y, a+b, gcd(x+y, a+b) );或使用tmp vars。甚至更糟糕的是,如果要编写该怎么办Fraction f( foo(x), bar(y) );-那么您将需要调用站点声明一些tmp var来保存返回值,或者再次调用这些函数并希望编译器CSE消除它们,这是我们要避免的。您是否要调试一个调用者将args混合在一起的情况,gcd以便实际上不是传递给构造函数的前两个args的GCD?没有?然后不要使该错误成为可能。
Peter Cordes
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.