【问题标题】:C++ - Are const parameters and class variables pessimization?C++ - 常量参数和类变量是否悲观?
【发布时间】:2016-06-09 20:04:41
【问题描述】:

我试图弄清楚在编写 C++ 代码时应该何时使用 const。这些都是悲观的例子还是这样写代码有好处?:

示例 1:

int findVal(const int OTHER_VAL) const
{
    switch(OTHER_VAL)
    {
    case 1:
        return 2;
    default:
        return 3;
    }
}

示例 2:

enum class MobType
{
    COW, CHICKEN, DOG, PIG
};

class BaseMob
{
protected:
    BaseMob(const MobType TYPE) : TYPE(TYPE) { }

    const MobType TYPE;
};

示例 3:

void showWorld(const World &world)
{
    auto data = world.getData();
    for (auto &i:data)
        i.print();
}

【问题讨论】:

  • 知道变量永远不会被修改意味着您在尝试理解它周围的代码时需要跟踪的事情更少。
  • “我想弄清楚在编写 C++ 代码时何时应该使用 const”
  • 他们中的任何一个会以什么方式成为悲观?
  • stackoverflow.com/q/117293/212870 的可能重复项。
  • 有时你假设一个方法不会改变它的类的状态。所以你在方法调用之后继续,就好像什么都没有改变一样。但是,如果该方法有很多嵌套调用,则可能会引入一个错误,其中一个会更改您不希望更改的类变量。将方法声明为const 是一种礼物,因为编译器会告诉您您的假设是否正确。这类错误很难被发现。

标签: c++ optimization parameters micro-optimization pessimistic


【解决方案1】:

不,他们不是。

const 使用自动存储的局部变量(包括函数 args)是纯粹的语法糖,可帮助人类程序员为其代码设置规则。它根本没有帮助优化器。优化编译器从 C 源代码中提取必要的数据移动,并对其进行优化。他们通常不在乎您是否将相同的 tmp 变量用于许多不同的事情,或者在同一函数中有 10 个不同的const tmp1 = a+10;

是的,这适用于按值传递的函数 args;它们是具有自动存储功能的局部变量,在寄存器或堆栈中传递。不,这并不意味着调用者可以假设一个函数没有修改用于参数传递的堆栈内存,因此它也没有帮助优化器。 (使用相同的参数进行第二次函数调用仍然需要将 args 重新写入堆栈(如果不是所有 args 都适合寄存器),因为 arg 上的 const 不会改变被调用函数“拥有" 该堆栈空间,可以根据需要将其用作暂存空间。)


const 在静态/全局/引用变量上确实有帮助。 static const int foo = 10; 可以作为直接常量内联,而不是从内存中加载。 (例如 add eax, 10 而不是 add eax, [foo])。


Using const to mark a class method as not changing any class members 还可以帮助编译器避免在函数调用后重新加载类成员。 (即,将它们保存在寄存器中)。这主要只适用于编译器看不到函数定义的情况,否则一个好的优化编译器可以只查看被调用函数的作用并进行相应的优化。 (只要它不在 Unix 库中,符号插入意味着它不能假定它在编译时看到的调用函数将是动态链接后调用的函数。)

【讨论】:

    【解决方案2】:

    只要您在逻辑上不更改值或对象,您就应该将其设为const。从逻辑上讲,我并不是指每次在技术上允许您这样做,而是每次在您的函数、类和代码的上下文中都是合乎逻辑的。

    一个简单的例子可能是一个简单的“get”函数,如例子 1 所示,这些函数不应修改类的状态,因此应标记为常量,因为这将有助于向用户记录您的意图,此外帮助您确保类的不变性。

    在某些情况下,创建不可变对象是有意义的,如示例 2 所示。我们在 C++ 中并不经常看到这些,但许多其他语言经常使用它们。如果它没有添加任何值以能够在对象生命周期内更改某个成员,那么您不妨将其设为 const。

    传递 const 引用参数可为您提供引用的性能优势,但同时确保源对象保持不变,这对用户来说既是很好的文档,也允许进行 som 优化。

    在提到所有这些原因之后,还有其他原因可以使用const,正如上一段中简要提到的,优化。当编译器知道某些东西是不变的并且没有被改变时,它可以启用一些非常聪明的优化,但出于性能原因不要使用const

    这也是为什么通过(例如)const_cast 强制转换(可以抛弃const)来解决 constness 会导致一些不良行为的原因。例如,请查看以下内容:

    #include <stdio.h>
    
    static const int foo = 10;
    
    int constsum(void) {
      return foo + 5;
    }
    
    int main(int argc, char* argv[]) {
      int a = constsum();
      int* newFoo = const_cast<int*>(&foo);
      *newFoo = 20;
      int b = constsum();
      printf("%d\n", a + b);
      return 0;
    }
    

    从这个示例 (see code running here) 可以看出,这可能不会产生预期的结果,因为代码导致打印 30,而不是预期的 40。

    在检查生成的程序集时,我们可以看到原因 (compiled into assembly):

    constsum():
            mov     eax, 15
            ret
    main:
            mov     eax, 30
            ret
    

    编译器只是内联值,因为它可以看到它们是常量,它不会特别注意 const_cast 正在使用。

    因此,const 正确性和const 的使用是一个有价值的工具,它可以提高代码的性能和稳定性,而且(不要忘记)它有助于记录您的代码。

    【讨论】:

    • 不受欢迎且确实未定义。
    • 您的 C 源代码与您的 asm 不匹配。你的godbolt链接有不同的来源,它只是返回值而不是把它提供给printf。我在挠头想知道这是否是 UB 试图通过指向只读内存的指针写入的结果......(我相信你知道,如果你做了一些实际上导致存储到 @987654335 @,它只是段错误。例如,将 newFoo 传递给存储到其中的非内联函数。但 void store(int a) { *const_cast&lt;int*&gt;(&amp;foo) = a; }gcc -O3 输出很有趣:只是一个 ret,即使使用 -Wall 也没有警告.)
    • void scan(char *p) { sscanf(p, "%d", const_cast&lt;int*&gt;(&amp;foo)); } 确实会生成警告。 godbolt.org/g/d8nJRP。我仍然认为在优化掉的情况下尝试通过const_casted 指针存储到foo 时根本没有任何警告,这很奇怪。 clang 甚至不会对 scanf 函数发出警告,除非你放弃演员表。 (然后它只是警告int *const int * 之间的类型不匹配,没有特定于传递一个已知指向只读内存的地址)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2015-11-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多