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

Posted

技术标签:

【中文标题】使用一个函数调用 C++ 初始化多个常量类成员【英文标题】:Initialize multiple constant class members using one function call C++ 【发布时间】:2020-07-16 21:42:43 【问题描述】:

如果我有两个不同的常量成员变量,它们都需要基于同一个函数调用进行初始化,有没有办法在不调用函数两次的情况下做到这一点?

例如,分子和分母为常数的分数类。

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 的输出分配给初始化列表中的那个,但这会导致内存浪费。

一般来说,有没有办法在不浪费函数调用或内存的情况下做到这一点?您能否在初始化列表中创建临时变量?

【问题讨论】:

你有证据证明“GCD 函数被调用了两次”吗?它被提到了两次,但这与编译器发出的代码调用它两次不同。编译器可能会推断它是一个纯函数,并在第二次提及时重用它的值。 @EricTowers:是的,在某些情况下,编译器有时可以在实践中解决这个问题。但前提是他们可以看到定义(或对象中的某些注释),否则无法证明它是纯的。您应该在启用链接时优化的情况下进行编译,但不是每个人都这样做。该函数可能在库中。或者考虑确实有副作用的函数的情况,并且只调用一次是正确的问题? @EricTowers 有趣的一点。我确实试图通过在 GCD 函数中放置一个 print 语句来检查它,但现在我意识到这会阻止它成为一个纯函数。 @Qq0:您可以通过查看编译器生成的 asm 来检查,例如将 the Godbolt compiler explorer 与 gcc 或 clang -O3 一起使用。但可能对于任何简单的测试实现,它实际上都会内联函数调用。如果您在原型上使用 __attribute__((const)) 或 pure 而不提供可见定义,它应该让 GCC 或 clang 在具有相同 arg 的两个调用之间进行公共子表达式消除 (CSE)。请注意,Drew 的答案甚至适用于非纯函数,因此它要好得多,您应该在 func 可能不内联的任何时候使用它。 一般来说,最好避免使用非静态 const 成员变量。 const一切不经常适用的少数几个领域之一。例如,您不能分配类对象。您可以将 emplace_back 放入向量中,但前提是容量限制不会影响大小调整。 【参考方案1】:

一般来说,有没有办法在不浪费函数调用或内存的情况下做到这一点?

是的。这可以通过在 C++11 中引入的delegating constructor 来完成。

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

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 You can observe here 启用适度优化没有开销。 @Qq0:C++ 是围绕现代优化编译器设计的。他们可以简单地内联这个委托,特别是如果你让它在类定义中可见(在 .h 中),即使真正的构造函数定义对于内联是不可见的。即gcd() 调用将内联到每个构造函数调用站点中,并且只留下一个 call 给 3 操作数私有构造函数。【参考方案2】:

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

#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) : numeratora/gcd(a,b), denominator(b/(a/numerator))
        
    
//private:
    const int numerator, denominator;//make sure that they are in this order
;
//Test
int main()
    Fraction f4,6;
    std::cout << f.numerator << " / " << f.denominator;

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

【讨论】:

好的,它特别适用于 GCD,但许多其他用例可能无法从 args 和第一个 const 派生第二个 const。正如所写的那样,这有一个额外的划分,这是另一个缺点,而不是编译器可能不会优化的理想。 GCD 可能只需要一个师,所以这可能几乎和调用 GCD 两次一样糟糕。 (假设除法在其他操作的成本中占主导地位,就像在现代 CPU 上经常发生的那样。) @PeterCordes 但另一个解决方案有一个额外的函数调用并分配更多的指令内存。 你说的是 Drew 的委托构造函数吗?这显然可以将Fraction(a,b,gcd(a,b)) 委托内联到调用者中,从而降低总成本。对于编译器来说,内联比撤销其中的额外除法更容易。我没有在godbolt.org 上尝试过,但如果你好奇的话可以试试。像普通构建一样使用 gcc 或 clang -O3。 (C++ 是围绕现代优化编译器的假设设计的,因此具有 constexpr 等功能)【参考方案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


只有这样,才不会再次调用函数、构造函数或其他方法,因此不会浪费时间。而且它不会浪费内存,因为无论如何都必须创建一个临时的,所以你不妨好好利用它。它还避免了额外的除法。

【讨论】:

您的编辑使它甚至无法回答问题。现在你要求调用者传递第三个参数?您在构造函数体内使用赋值的原始版本不适用于const,但至少适用于其他类型。您“还”避免了哪些额外的划分?你的意思是 vs. asmmo 的回答? 好的,既然你已经解释了你的观点,就删除了我的反对意见。但这显然很糟糕,并且需要您手动将一些构造函数内联到每个调用者中。这与 DRY(不要重复自己)和封装类的职责/内部结构相反。大多数人不会认为这是一个可以接受的解决方案。鉴于有一种 C++11 方法可以干净地做到这一点,除非他们被旧的 C++ 版本卡住,否则任何人都不应该这样做,并且该类对该构造函数的调用很少。 @aconcernedcitizen:我的意思不是出于性能原因,而是出于代码质量原因。以你的方式,如果你改变了这个类在内部的工作方式,你必须找到所有对构造函数的调用并改变第三个参数。额外的,gcd(foo, bar) 是额外的代码,可以并且因此应该从源中的每个调用站点 中排除。这是一个可维护性/可读性问题,而不是性能问题。编译器很可能会在编译时内联它,这是您想要的性能。 @PeterCordes 你说得对,现在我看到我的注意力已经集中在解决方案上,而我忽略了其他一切。无论哪种方式,答案都会保留,即使只是为了羞辱。每当我对此有疑问时,我就会知道去哪里寻找。 还要考虑 Fraction f( x+y, a+b ); 的情况,要按照自己的方式编写,您必须编写 BadFraction f( x+y, a+b, gcd(x+y, a+b) ); 或使用 tmp 变量。或者更糟糕的是,如果你想写 Fraction f( foo(x), bar(y) ); 怎么办 - 那么你需要调用站点声明一些 tmp 变量来保存返回值,或者再次调用这些函数并希望编译器 CSE 将它们删除,这就是我们的重新避免。您是否要调试一个调用者将 args 与 gcd 混合的情况,因此它实际上不是传递给构造函数的前 2 个 args 的 GCD?不?那就不要让这个错误成为可能。

以上是关于使用一个函数调用 C++ 初始化多个常量类成员的主要内容,如果未能解决你的问题,请参考以下文章

《C++程序设计POJ》《WEEK3 类和对象进阶》成员对象和封闭类/友元/this指针/常量成员函数

C++的构造函数的作用:初始化类对象的数据成员

C++的构造函数的作用:初始化类对象的数据成员

C++的构造函数的作用:初始化类对象的数据成员

C++:首先调用/初始化哪个?其成员变量的类构造函数或构造函数?

在 C++ 成员函数中使用“静态”关键字限制访问