程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> C語言 >> C++ >> 關於C++ >> Effective Modern C++ 條款15 盡可能使用constexpr

Effective Modern C++ 條款15 盡可能使用constexpr

編輯:關於C++

盡可能使用constexpr

如果要選出C++11中最讓人迷惑的新關鍵字,那麼大概是constexpr。當constexpr用於對象時,它本質上就是加強版的const,但它用於函數時,它擁有不同的意思。constexpr再迷惑,也是值得的,因為當constexpr與你想要表達的一致時,你肯定會用它。

在概念上,constexpr表明一個值不僅是常量,還是在編譯期間可知。這概念只是拼圖的一部分,因為當constexpr用於函數時,有點微妙的區別。免得我破壞了最後的驚喜,我現在只可以說,你不能假定constexpr函數的返回結果是const的,也不能理所當然的人物它們的返回值在編譯期間可知。可能會很有趣,這些特性。constexpr函數不需要返回const結果和編譯器可知結果,這是有益的。

不過我們還是先講constexpr對象,這些對象呢,事實上和const一樣,它們的值在編譯期間就知道了。

那些在編譯期間就可知的值是享有特權的。例如,它們可能存放在只讀的內存區域中,特別是為那些內嵌系統的開發者,這是一個相當重要的特性。在C++的上下文中需要一個整型常量表達式(integral constant expression)時,一個常量的和編譯期間可知的整型數具有廣泛適應性。這種上下文包括數組大小的表示,整型模板參數(包括std::array對象的長度),枚舉的值,對齊說明,等等。如果你想要一個變量,用於剛說的東西,那麼你肯定想要把那個變量聲明為constexpr,因為編譯器會確保它在編譯期間有值:
int sz; // non-constexpr variable
...
constexpr auto arraySize1 = sz; // 錯誤,編譯期間不知道sz的值

std::array data1; // 錯誤,同樣的問題

constexpr auto arraySize2 = 10; // 正確,10在編譯期間是常量

std::array data2; // 正確,arraySize2是constexpr

請注意const並不提供與constexpr相同的保證,因為const對象在編譯時不需要用已知的值初始化:
int sz; // 如前
...
const auto arraySize = sz; // 正確,arraySize是sz的const拷貝

std::array data; // 錯誤,arraySize的值在編譯期間不可知

我們可以簡單地認為,所有constexpr對象都是const的,但是不是所有的const對象都是constexpr的。如果你想要編譯器保證變量編譯期有值,即上下文請求了一個編譯期間的常量,那麼能用的工具是constexpr,而不是const

當涉及到constexpr函數的時候,constexpr對象的使用會變得更加有趣。當編譯期間的常量作為參數傳遞給constexpr函數時,這種函數會返回編譯期間常量。如果函數的參數在運行期間才能知道,函數返回的也是運行時的值。聽起來有點亂,正確的規則:

constexpr函數可以用在需求編譯期間常量的上下文。在這種上下文中,如果你傳遞參數的值在編譯期間已知,那麼函數的結果會在編譯期間計算。如果任何一個參數的值在編譯期間未知,代碼將不能通過編譯。 如果用一個或者多個在編譯期間未知的值作為參數調用constexpr函數,函數的行為和普通的函數一樣,在運行期間計算結果。這意味著你不需要用兩個函數來表示這個操作——一個在編譯期間和一個在運行期間。constexpr函數具有兩個動作。

假設我們需要一個數據結構來保存某個實驗的結果,這個實驗可在不同的條件下進行。例如,在實驗期間,光的強度可高可低,風速和溫度也可變化。如果與實驗有關的環境條件有n個,每個環境變量又有3種狀態,那麼就有3^n種情況。存儲實驗可能出現的所有結果,就要求數據結構有足夠大的空間保存3^n個值。假設每個結果是int值,然後n在編譯期間已知(或者可計算),那麼選擇std::array這數據結構將會合情合理。C++標准庫提供std::pow,是我們需要的數學計算函數,但這裡會有兩個問題。第一,std::pow作用於兩個浮點型指針,而我們需要的是一個整型結果。第二,std::pow不是constexpr的,所以我們不能用它的結果來指定std::array的值。

幸運的是,我們可以自己寫pow函數。等下我會展示它是怎麼做的,但我們先看看它是怎樣聲明和使用的:
constexpr // pow是個constexpr函數
int pow(int base, int exp) noexcept // 函數不會拋出引出
{
... // 實現看下面
}

constexpr auto numCouds = 5; // 條件個數

std::array results; // results有3^n個元素

constexprpow並不是說明pow返回const值,它指的是,如果base和exp是編譯期間常量,pow的結果可以被用作編譯期間常量。如果base和(或)exp不是編譯期間常量,pow的結果將會在程序運行時計算,這意味pow不僅可以在編譯期間計算std::array的大小,還可以在運行期間的上下文調用:
auto base = readFromDB("base"); // 在運行期間
auto exp = readFromDB("exponent"); // 獲取值

auto baseToExp = pow(base, exp); // 在運行期間調用pow

因為用編譯期間的值作為參數調用constexpr函數一定要返回編譯期間的結果,所以會有限制強加於它們的實現。C++11和C++14的限制不同。

在C++11,constexpr只能有一個return語句。聽起來不是什麼限制,因為可以用兩個技巧。第一個是“?:”運算符代替if-else語句,第二個是可以用遞歸。所以pow可以這樣實現:
constexpr int pow(int base, int exp) noexcept
{
return(exp == 0 ? 1 : base * pow(base, exp - 1));
}

這可以運行,但是很難想象除了大神還有誰能把它寫得這麼好。在C++14中,constexpr函數的限制大幅寬松,所以這種函數實現成為可能:
constexpr int por(int base, int exp) noexcept
{
auto result = 1;
for (int i=0; i < exp; ++i) result *= base;
return results;
};

constexpr函數限制持有和返回的類型為字面值類型(literal type),本質上就是一些在編譯期間可確定值的類型。在C++中,除了void之外的內置類型都是字面值類型,不過用戶定義的類型也有可能是字面值類型,因為構造函數和其他成員函數可能是constexpr的:
class Point {
public:
constexpr Point(double xVal = 0, double yVal = 0) noexcept
: x(xVal), y(yVal)
{}

constexpr double xValue() const noexcept { return xVal; }
constexpr double yValue() const noexcept { return yVal; }

void setX(double newX) noexcept { x = newX; }
void setY(double newY) noexcept { y = newY; }

private:
double x, y;
};

在這裡,Point的構造函數可以被聲明為constexpr,因為如果傳進來的參數在編譯時就可以知道,那麼由P構造的成員變量的值在編譯時也可以被知道。因此Point可以用constexpr初始化:
constexpr Point p1(9.4, 27.7); // 正確,在編譯時“運行”constexpr構造

constexpr Point p2(28.8, 5.3); // 也正確

同樣的,獲取函數(getter)xValue和yValue也可以是constexpr,因為如果它們被一個編譯期間已知的Point對象調用(例如,一個constexpr的Point對象),成員變量x和y的值在編譯時是已知的,這使一個constexpr函數調用Point的獲取函數並用其結果來初始化一個constexpr對象成為可能:
constexpr
Point midpoint(const Point &p1, const Point &p2) noexcept
{
return { (p1.xValue + p2.xValue)) / 2, // 調用constexpr
(p1.yValue + p2.yValue)) / 2 }; // 成員函數
}

constexpr auto mid = midpoint(p1, p2); // 用constexpr函數的結果
// 初始化constexpr對象。

這很亦可賽艇,這意味著對象mid的初始化涉及到構造函數、獲取函數、非成員函數的調用,然後創建在只讀內存區域!這意味著你可以將一個類似mid.xValue() * 10的表達式用於模板參數或者一個指定枚舉值的表達式!這意味著傳統意義上,編譯期需完成的工作與運行期間需完成的工作之間的嚴格清晰的線變模糊了,而一些傳統意義上運行時的工作可以遷移到編譯期。參與遷移的代碼越多,軟件運行得越快(但是,編譯的時間可能變長)。

在C++11,有兩個限制因素妨礙把Point的成員變量setX和setY聲明為constexpr。第一,它們改變了它們操作的值,然後在C++11,constexpr成員函數是隱式聲明為const的。第二,它們的返回類型是void,然後在C++11,void不是字面值類型。都是這兩個限制在C++14被解除了,所以在C++14,設置函數(setter)也可以constexpr
class Point {
public:
...
constexpr void setX(double newX) noexcept // C++14
{ x = newX; }
constexprvoid setY(double newY) noexcept // C++14
{ y = newY; }
...
};

這使得寫這奇葩的函數成為可能:
// 返回p的映像(C++14)
constexpr Point reflection(const Point &p) noexcept
{
Point result; // create non-const Point

result.setX(-p.xValue());
result.setY(-p.yValue());

return result;
}

用戶的代碼可能是這樣的:
constexpr Point p1(9.4, 27.7);
constexpr Point p2(28.8, 5.3);
constexpr auto mid = midpoint(p1, p2);

constexpr auto reflectedMid = // reflectedMid的值是(-19.1, -16.5)
reflection(mid);// 而且在編譯期間就知道了

本條款的建議是盡可能使用constexpr,然後現在我希望你能很清楚為什麼:constexpr對象和constexpr函數比起non-constexpr對象和函數具有更廣泛的語境。通過盡可能地使用constexpr,你最大化了對象和函數的可能使用的情況。

注意到constexpr是一個對象或函數接口的一部分是很重要的。constexpr表明“我可以用於需求常量表達式的上下文”,如果你把對象或者函數聲明為constexpr,用戶就有可能把它用於這種上下文。後來,如果你覺得你使用constexpr是個錯誤,然後你刪除了它,這樣就可能造成用戶大量代碼無法編譯(為了調試條件I/O函數會導致這種問題,因為I/O語句通常不允許出現在constexpr函數)。“盡可能使用constexpr”中的“盡可能”是你願意作出長期的承諾,強行約束著constexpr的對象和函數(這句話太難了,我不知道我的理解有沒問題:Part of “whenever possible”in “Use constexpr whenever possible” is your willingness to make a long-term commitment to the constraints it imposes on the objects and functions you apply it to.)。··

總結

需要記住4點:

constexpr對象是const的,它需用編譯期間已知的值初始化。 constexpr函數在傳入編譯期已知值作為參數時,會在編譯期間生成結果。 constexpr對象和函數比起non-constexpr對象和函數具有更廣泛的語境。 constexpr是對象和函數接口的一部分。
  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved