最近在使用constexpr的時候無意中踩了個小坑。
下面給個小示例:
#include <iostream>
constexpr int n = 10;
constexpr char *msg = "Hello, world!";
int main()
{
for (auto i = 0; i < n; ++i) {
std::cout << msg << std::endl;
}
}
constexpr應該是大家很熟悉的東西了,也是最常用的c++11新特性之一。和巨集相比除了更強的型別安全之外,constexpr還帶來了編譯期計算。
上面的程式碼相當簡單,我們迴圈輸出“Hello, world!”這個字串10次。
這麼簡單的程式碼還有討論的必要嗎?一開始我也是這麼想的,然而當我們編譯執行的時候卻會得到下面這樣的警告:
$ g++ --version
g++ (GCC) 10.2.0
Copyright (C) 2020 Free Software Foundation, Inc.
This is free software; see the source for copying conditions. There is NO
warranty; not even for MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
$ g++ -std=c++17 -Wall -Wextra test.cpp
test.cpp:4:23: 警告: ISO C++ forbids converting a string constant to ‘char*’ [-Wwrite-strings]
4 | constexpr char *msg = "Hello, world!";
| ^~~~~~~~~~~~~~~
這段資訊的意思是c++不允許把字串字面量賦值給char*
。
然而對於constexpr,文件中是這麼寫的:
A constexpr specifier used in an object declaration implies const.
這裡的object你可以理解為變數,意思是constexpr修飾的變數都會隱式新增一個const限定符。
也就是說:
// T 是任意型別
constexpr T a = xxx;
// 不考慮其他因素,在型別上等價於:
T const a = xxx;
我們這裡的T
實際上可以填任意型別,包括指標。這不是說明我們的指標變數有const嗎?
眼尖的讀者大概已經知道答案了:constexpr新增的是頂層const。
所以我們的程式碼實際上是這樣的:
// 原本的程式碼
constexpr char *msg = "Hello, world!";
// 實際上的效果
char * const msg = "Hello, world!";
下面一行的msg
實際上是一個指向char
的指標常量,而我們可以通過它任意修改被指向的字串(當然這是未定義行為)。指標常量意味著我們不能把這個指標重新指向其他的物件,這個const作用在指標本身上,因此叫做頂層const。
而字串常量的型別是const char[N]
,在表示式裡退化為const char *
,這表示一個指向常量字串的指標,這裡的const的底層的,因為它作用於被指向的物件而不是我們的指標自身。
對於頂層const,賦值的時候是可以被去除的,而底層const則不行,這就是為什麼編譯器會彈出警告的原因了。
正確的做法也很簡單,牢記constexpr不是const的等價替代品,它只會新增頂層const,不會新增底層const。
所以constexpr的字串常量應該這樣寫:
constexpr const char *p = "Hello, world!";
或者你的編譯環境支援c++17,我更推薦你這樣寫:
#include <string_view>
constexpr std::string_view msg = "Hello, world!";
使用string_view
之後就不會出現上面的頂層/底層const的坑了。所以在現代c++裡能不用裸指標就儘量不要用。