首页 > 编程语言 >c++ 中 const, constexpr 的使用

c++ 中 const, constexpr 的使用

时间:2022-10-27 12:02:13浏览次数:45  
标签:index const int mov c++ constexpr return

目录

C++ 与 C 语言相比有着更强的类型检查,包括四种 cast,左值右值之分,reference,以及最重要的——对 const 的要求。

const 是一个相当麻烦的要求,比如其强大的“传播性”——只要在一个地方使用,就可能蔓延到各个角落,出现各种编译错误。但编程实践证明 const 的使用是值得的(甚至 Rust 语言已经将 const 作为基本要求了),可以提高代码的安全性和可理解性。

这种要求会体现在很多方面。

char a[] = "Hello";
char *p = a;
const char *q = a;
char *const r = a;
const char *const s = a;

你能说出它们有什么区别吗? (顶层 const 和底层 const

同时在函数的参数、返回值中也要考虑 const

参数

bool longerThan1(const string &s1, const string &s2) {
    return s1 > s2;
}
bool longerThan2(string &s1, string &s2) {
    return s1 > s2;
}
string a = "Hello";
longerThan1(a, "World"); // Yes
longerThan2(a, "World"); // No

不加 const 然编译器认为你想要对参数做出某些改变,而 "World" 可不是一个真正的 string 对象,自然不能让你改变它。

在这里有人认为可以使用下列函数

bool longerThan3(string s1, string s2) {
    return s1 > s2;
}
longerThan3(a, "World"); // ?

然而这是欠缺考虑的形式

  • 对于 a 来说,longerThan3()a 完完全全地复制了一遍,而 longerThan1() 使用的 const reference 实质是一种指针,不会复制 alongerThan3() 花费了无用的时间和空间。
  • 对于 "World" 来说,longerThan1(), longerThan3() 都使用 "World" 创建了一个新的 string 对象,在这一点上是一样的;但 longerThan1() 创建的 s2 是带有 const 的,这防止了对 s2 无谓的修改。

因此在任何时候,能用 const 就用 const

还要考虑最特别的参数 this,最好的实践同样是是:不改变成员的函数全都使用 const this。同时,对某些函数可以使用重载,比如

class Array
{
public:
    const int &operator[](size_t index) const {
        return a[index];
    }
    int &operator[](size_t index) {
        return a[index];
    }

private:
    int *a;
};

void printArray(const Array &a) {
    for (size_t i = 0, x = a.size(); i != x; ++i) cout << a[i];
}

const ArrayArray 可以使用有 const this 的函数,这点在传递 const Array &a 作为其他某个函数的参数的时候使用。

例外

不过如果真的遇到了需要用到用值传递的函数参数,则应该避免使用 const,例如

void foo(int x);       // Yes
void foo(const int x); // No

因为这属于多此一举,用值传递的参数只有函数内可以看见和使用,不会影响到原来的那个变量。这属于限制了函数的创建者,却没影响到函数的调用者。整个函数的逻辑都应该被创建者掌握,又何必多加限制呢。

返回值

对于返回值也是这样,只要稍加考虑就能明白,在绝大多数情况我们的返回值都应该是常量。因为返回值通常被赋值给另一个对象,亦或是弃之不用。考虑如下函数:

class B
{
public:
	B(int bb) : b(bb) {}
	B() : B(0) {}
	friend B operator+(const B &lhs, const B &rhs) {
        return B(lhs.b + rhs.b);
    } // 只是在类里面实现friend函数罢了

private:
	int b;
};

result = B(3) + B(5);

看上去,resultoperator+() 的返回值,然而实际情况却是这样

__t = B(3) + B(5);
result = __t;

__t 才是返回值,然后被赋值给 result。因此完全有可能出现这种情况。

B(3) + B(5) = 5;

虽然做法比较离奇,但谁知道调用者会怎么做呢(也有可能是 == 打成了 =),还是使用 const 为返回值加上限制吧。 friend const B operator+(const B &lhs, const B &rhs) { return B(lhs.b + rhs.b); }

例外

考虑 STL 的实践

vector<int> v{1, 2, 3};
for (auto p = ++v.begin(), q = --v.end(); p != q; ++p) cout << *p;

这里的 ++ -- 就说明没有为 begin(), end() 的返回值加上 const,这是为了方便使用者调整。

const this 和成员

值得注意的是,对于上述的 class Array,下面这样的函数是可行的。

int &operator[](size_t index) const {
    return a[i];
}

这是因为,const this 所代表的是:这个类成员不能变动,而 a(指针)是 Array 的成员,但 a[index](指针所指的位置)却不是 Array 的成员

至于此时 const 该不该加则需要一些考虑(在 bitwise constnesslogical constness 之间)。对于上面的这种情况,则应该还是要加的,因为这里 Array 是数组类。虽然编译器不认为 a[index] 是成员,但从逻辑上讲,考虑到我们实现 Array 的目的——作为一个容器类,a[index] 是成员。

而在另外的一些情况则可以不加。不仅如此,C++ 还提供了一个关键字 mutable 让你去掉 const 限制,也就是说,即使在是成员的情况下也可以修改某些量。加或不加,这是一个问题。

const_cast

const_cast 最大的作用就是在加或不加 const 的重载函数间提供方便。

比如 class Array 中的 operator[]() 可以改写成

const int &operator[](size_t index) const {
    return a[index];
}
int &operator[](size_t index) {
    return const_cast<int &>(const_cast<const A &>(*this)[index]);
}

比起原来的是不是还要麻烦许多?但是你看看这个

const int &operator[](size_t index) const {
	if (index < 0 || index >= size())
		throw std::invalid_argument("index out of bound");
#ifdef DEBUG
// recording
#endif // DEBUG
    return a[index];
}
int &operator[](size_t index) {
    return const_cast<int &>(const_cast<const A &>(*this)[index]);
}

就不用写两遍了,减少了很多麻烦。必须指出的是,只可能在 non-const 函数中用这种方法调用 const 函数,反之则不行。

constexpr 的关系

实际上两者没有什么关系,对于 constexpr,我称为真正的常量。在编译器就可以展开,可以用在 static_assert 中。用法有三种

  • 变量
  • 函数
  • 构造函数
函数

对函数使用后,能在编译期就可以直接算出值,但也有要求

  • 所有相关变量、函数都在编译期可以求得。这是总要求,其他要求都是衍生。
  • 参数是常量;
  • 除了 using, typedef, static_assert 只能有 return 语句;
  • 调用的函数也得是 constexpr
  • 不能使用全局变量等可能在运行期改变的量;
  • 返回常量。
constexpr int add(int x, int y) { return x + y; }

int main() {
	int k = add(3, 5);   // 常量版本
	int t = add(k, 2);   // 非常量版本
}

看看反汇编,可以被理解为常量的就直接就算出来了

0x000000000000073a <+0>:     push   %rbp
0x000000000000073b <+1>:     mov    %rsp,%rbp
0x000000000000073e <+4>:     sub    $0x10,%rsp
0x0000000000000742 <+8>:     movl   $0x8,-0x8(%rbp)       ; k 在这
0x0000000000000749 <+15>:    mov    -0x8(%rbp),%eax
0x000000000000074c <+18>:    mov    $0x2,%esi
0x0000000000000751 <+23>:    mov    %eax,%edi
0x0000000000000753 <+25>:    callq  0x7c0 <_Z3addii>      ; 只调用一次
0x0000000000000758 <+30>:    mov    %eax,-0x4(%rbp)       ; t 在这
0x000000000000075b <+33>:    mov    $0x0,%eax
0x0000000000000760 <+38>:    leaveq 
0x0000000000000761 <+39>:    retq   

要注意的是,定义要放在调用前,像下面这样就不能识别了

constexpr int add(int x, int y);

int main() {
	int k = add(3, 5);
	int t = add(k, 2);
}

constexpr int add(int x, int y) { return x + y; }
0x000000000000073a <+0>:     push   %rbp
0x000000000000073b <+1>:     mov    %rsp,%rbp
0x000000000000073e <+4>:     sub    $0x10,%rsp
0x0000000000000742 <+8>:     mov    $0x5,%esi
0x0000000000000747 <+13>:    mov    $0x3,%edi
0x000000000000074c <+18>:    callq  0x7cb <_Z3addii>
0x0000000000000751 <+23>:    mov    %eax,-0x8(%rbp)            ; k
0x0000000000000754 <+26>:    mov    -0x8(%rbp),%eax
0x0000000000000757 <+29>:    mov    $0x2,%esi
0x000000000000075c <+34>:    mov    %eax,%edi
0x000000000000075e <+36>:    callq  0x7cb <_Z3addii>
0x0000000000000763 <+41>:    mov    %eax,-0x4(%rbp)            ; t
0x0000000000000766 <+44>:    mov    $0x0,%eax
0x000000000000076b <+49>:    leaveq 
0x000000000000076c <+50>:    retq   
变量

对变量使用,变成常量,没什么好说的,主要注意在类中的情况

class B
{
    constexpr int           a = 10;   // Wrong: need to be static
    static constexpr int    b = 10;   // Right
    static constexpr double c = 10;   // Right
    static const int        d = 10;   // Right
    static const double     e = 10;   // Wrong: non-integral type
    static const double     f;        // Right
};
const B::f = 10;

使用 constexpr 可以直接在类中初始化 double 常量

构造函数

可以像内置类型一样定义常量,其他要求大致和函数一样。

class B
{
public:
	constexpr B(int bb) : b(bb) {}
private:
	int b;
};

int foo() {
    constexpr B b(55);
}

C++17、C++20 还有更多用法,不过就不讲了,现在用的最多的还是 C++14。

标签:index,const,int,mov,c++,constexpr,return
From: https://www.cnblogs.com/violeshnv/p/16831724.html

相关文章

  • C++算法之旅、02 从木棒切割问题领悟二分法精髓
    172、木棒切割问题https://sunnywhy.com/problem/172题目描述给出n根木棒的长度,现在希望通过切割它们来得到至少k段长度相等的木棒(长度必须是整数),问这些长度相等的木......
  • P5377 鸽鸽的分割 评论及c++题解
    P5377鸽鸽的分割1.原题连接2.评论下位红(划掉简单题只需要推导出公式或分类讨论就行了这里只给出公式解法根据题意在一个圆上确定n(n∈正整数)个点,求最多可被......
  • C++函数指针和回调函数
    C++函数指针和回调函数在C++中函数指针名就是函数的地址//定义函数指针:返回类型(*pfunc)(形参列表)void(*pfunc)(int,string);int(*pfunc)(int,string,double);......
  • C++ 面向对象高级开发(四) Sting类 浅谈
    StringClass 带指针的Class不能用默认拷贝  构造函数、拷贝构造、拷贝赋值、析构函数   浅拷贝导致内存泄漏两个指针指一个  深拷贝  ......
  • C++ 面向对象高级开发 (五) 栈堆、new和delete
                   ......
  • 【leetcode_C++_栈与队列_day9】232.用栈实现队列&&225. 用队列实现栈
    知识补充:栈与队列理论基础(C++)C++中stack是容器么?​ stack:堆栈栈是以底层容器完成其所有的工作,对外提供统一的接口,底层容器是可插拔的(也就是说我们可以控制使用哪种......
  • C++标准库字符串流sstream
    sstream与strstream在C++有两种字符串流,一种在<strstream>中定义,另一种在<sstream>中定义,两者的区别如下:strstream里包含strstreambuf、istrstream、ostrstream、strst......
  • C++性能优化指南 电子书 pdf
    作者:KurtGuntheroth出版社:人民邮电出版社原作名:OptimizedC++:ProvenTechniquesforHeightenedPerformance译者:杨文轩 链接:C++性能优化指南  本书......
  • c++ template
    template<typenameT,typenameM>//基本的模板函数和模板类voidtestfunc(Ta,Mb){std::cout<<a<<b<<std::endl;}template<typenameT,typenameN>c......
  • C++模板元编程实战 电子书 pdf
    作者:李伟出版社:人民邮电出版社副标题:一个深度学习框架的初步实现 链接:C++模板元编程实战  《C++模板元编程实战:一个深度学习框架的初步实现》以一个深度学......