Item 15: 只要有可能,就使用constexpr

本文翻譯自《effective modern C++》,由於水平有限,故無法保證翻譯完全正確,歡迎指出錯誤。謝謝!

博客已經遷移到這裏啦

如果說C++11中有什麼新東西能拿“最佳困惑獎”的話,那肯定是constexpr了。當把它用在對象上時,它本質上是const的加強版,但是把它用在函數上時,它將擁有不同的意義。切開“迷霧”(解開困惑)是值得的,因爲當constexpr符合你想表達的情況時,你肯定會想要使用它的。

從概念上來說,constexpr表明的一個值不只是不變的,它還能在編譯期被知道。但是這個概念只是故事的一部分,因爲當constexpr應用在函數上時,事情變得比看上去還要微妙。爲了避免毀掉後面的驚喜,現在,我只能說你不能假設constexpr函數的返回值是const的,同時你也不能假設這些值能在編譯期被知道。也許最有趣的是,這些東西都是特性(是有用的)。對於constexpr函數來說,不需要產生const或能在編譯期知道的返回結果是一件好事。

但是,讓我們從constexpr對象開始。這些對象確實是常量,也確實能在編譯期被知道。(技術上來講,它們的值是在翻譯階段被決定的,翻譯階段包含了編譯期和鏈接期。除非你要寫一個C++的編譯器或連接器,不然這都影響不到你,所以你能在編程的時候,開心地假設爲constexpr對象的值是在編譯期被決定的)

值能在編譯器知道是很有用的。它們能代替只讀內存,舉個例子,尤其是對一些嵌入式系統來說,這是一個相當重要的特性。更廣泛的應用就是,當C++要求一個不變的,並且在編譯期能知道的整形常量表達式的時候,我們可以使用它(constexpr對象或函數)來替代。這樣的場景包括了數組大小的說明,整形模板參數(包括std::array對象的長度),枚舉成員的值,alignment說明符,等等。如果你想使用變量來做這些事,你肯定想要把它聲明爲constexpr,因爲編譯器會確保它是一個編譯期的值:

int sz;                             //non-constexpr變量

...

constexpr auto arraySize1 = sz;     //錯誤!sz的值不是在編譯期被知道的

std::array<int, sz> data1;          //錯誤!同樣的問題

constexpr auto arraySize2 = 10;     //對的,10是一個編譯期的常量

std::array<int, arraySize2> data2;  //對的,arraySize2是一個constexpr

記住,const不能提供和constexpr一樣的保證,因爲const對象不需要用“在編譯期就知道的”值初始化:

int sz;

...

const auto arraySize = sz;          //對的,arraySize是拷貝自sz的const變量

std::array<int, arraySize> data;    //錯誤!arraySize的值不能在編譯期知道

簡單來說,所有的constexpr對象都是const對象,但是不是所有的const對象都是constexpr對象。如果你想讓編譯器保證變量擁有的值能被用在那些,需要編譯期常量的上下文中,那麼你就應該使用constexpr而不是const。

當涉及constexpr函數時,constexpr對象的使用範圍變得更加有趣。當使用編譯期常量來調用這樣的函數時,它們產生編譯期常量。當用來調用函數的值不能在運行期前得知時,它們產生運行期的值。這聽起來好像你知道它們會做什麼,但是這麼想是錯誤的。正確的觀點是這樣的:

  • constexpr函數能被用在要求編譯期常量的上下文中,如果所有傳入constexpr函數的參數都能在編譯期知道,那麼結果將在編譯期計算出來。如果有任何一個參數的值不能在編譯期知道,你的代碼就被拒絕(不能在編譯期執行)了。

  • 當使用一個或多個不能在編譯期知道的值來調用一個constexpr函數時,它表現得就像一個正常的函數,在運行期計算它的值。這意味着你不需要兩個函數來表示相同的操作,一個爲編譯期常量服務,一個爲所有的值服務。constexpr函數把這些事都做了。

假設你需要一個數據結構來存放一個運算方式不會改變的實驗結果。舉個例子,在實驗過程中,燈的亮度等級(有高,低,關三種狀態),風扇的速度,以及溫度也是這樣,等等。如果這裏有n種環境條件和實驗有關,每種條件有三種狀態,那麼結果的組合數量就是3^n。因此對於實驗結果的所有的組合進行保存,就需要一個起碼有3^n的空間的數據結構。假設每個結果都是一個int,那麼n就是在編譯期已知的(或者說可以計算出來),std::array是一個合理的選擇。但是我們需要一個方法來在編譯期計算3^n。C++標準庫提供了std::pow,這個函數是我們需要的數學函數,但是對於我們的目的來說,它有兩點問題。第一,std::pow在浮點類型下工作,但是我們需要一個整形的結果。第二,std::pow不是constexpr(也就是,用編譯期的值調用它時,它不能返回一個編譯期的結果),所以我們不能用它來明確std::array的大小。

幸運的是,我們能自己寫一個我們所需要的pow,我馬上就會告訴你怎麼實現,但是現在先讓我們看一下它是怎麼聲明以及怎麼使用的:

constexpr                                   //pow是一個constexpr函數
int pow(int base, int exp) noexcept         //永遠不會拋出異常
{
    ...                                     //實現在下面
}

constexpr auto numConds = 5;                //條件的數量

std::array<int, pow(3, numConds)> results;  //結果有3^numConds個函數

回憶一下,pow前面的constexpr不是說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函數時,必須能返回一個編譯期的結果,所以有些限制被強加在constexpr函數的實現上。C++11和C++14有不同的限制。

在c++11中,constexpr函數只能包含一條簡單的語句:一個return語句。實際上,限制沒聽起來這麼大,因爲兩個技巧可以用來擴張constexpr函數的表達式,並且這將超過你的想象。第一,條件表達式 “?:”能用來替換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 pow(int base, int exp) noexcept           //C++14
{
    auto result = 1;
    for(int i - 0; i < exp; ++i) result *= base;

    return result;
}

constexpr函數由於限制,只能接受和返回literal類型(本質上來說就是,這個類型的值能在編譯期決定)。在C++11中,除了void的所有built-in類型都是literal類型,user-defined類型也可能是literal類型。因爲構造函數和其他函數也可能是constexpr:

class Point{
public:
    constexpr Point(double xVal = 0, double yVal = 0) noexcept
        : x(xVal), y(yVal)
    {}

    constexpr double xValue() const noexcept { return x;}
    constexpr double yValue() const noexcept { return y;}

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

private:
    double x, y;
};

這裏,Point的構造函數被聲明爲constexpr,因爲如果傳入的參數能在編譯期知道,則被構造的Point的成員變量的值也能在編譯期知道。因此,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的getter,然後用這個函數的返回值來初始化一個constexpr對象。

constexpr
Point midpoint(const Point& p1, const Point& p2) noexcept
{
    return { (p1.xValue() + p2.xValue()) / 2,   //調用constexpr成員函數
             (p1.yValue() + p2.yValue()) / 2};  //並通過初始化列表產生一個
                                                //新的臨時Point對象
}

constexpr auto mid = midpoint(p1, p2);          //用constexpr函數的返回值
                                                //來初始化一個constexpr對象

這是很激動人心的,它意味着,雖然mid對象的初始化需要調用構造函數,getter函數和一個non-member函數,但是它還是能在read-only內存中創建!這意味着,你能使用一個表達式(比如mid.xValue() * 10)來明確模板的參數,或者明確enum成員的值。它意味着以前運行期能做的工作和編譯期能做的工作之間的界限變得模糊了,一些以前只能在運行期執行的運算現在可以移到編譯期來執行了。移動的代碼越多,軟件跑得越快。(當然編譯時間也會增加。)

在C++11中,有兩個限制阻止Point的成員函數setX和setY被聲明爲constexpr。第一,它們改動了它們操作的對象,但是在C++11中,constexpr成員函數被隱式聲明爲const。第二,它們的返回值類型是void,void類型在C++11中不是literal類型。在C++14中,兩個限制都被移除了,所以C++14的Point,能把它的setter也聲明爲constexpr:

class Point{
public:
    ...

    constexpr void setX(double newX) noexcept       //C++14
    { x = newX; }

    constexpr void setY(double newY) noexcept       
    { y = newY;}

    ...
};

這使得我們能寫出這樣的函數:

constexpr Point reflection(const Point& p) noexcept
{
    Point result;                       //創建一個non-constPoint

    result.setX(-p.xValue());           //設置它的x和y
    result.setY(-p.yValue());

    return result;                      //返回一個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);                    //並且是在編譯期知道的

本Item的建議是,只要有可能就使用constexpr,並且現在我希望你能知道這是爲什麼:比起non-constexpr對象和non-constexpr函數,constexpr對象和constexpr函數都能被用在更廣泛的上下文中(一些只能使用常量表達式的地方)。通過“只要有可能就使用constexpr”,你能讓你的對象和函數的使用範圍最大化。

記住,constexpr是對象接口或函數接口的一部分,constexpr宣稱“我能被用在任何需要常量表達式的地方”。如果你聲明一個對象或函數爲constexpr,客戶就有可能使用在這些上下文中(要求常量表達式的地方)。如果你之後覺得對於constexpr的使用是錯誤的,然後移除了constexpr,這會造成很大範圍的客戶代碼無法編譯。(由於調試的原因,增加一個I/O操作到我們的constexpr函數中也會導致同樣的問題,因爲I/O語句一般不允許在constexpr中使用)“只要有可能就使用constexpr”中的“只要有可能”是說:需要你保證你願意長時間保持這些對象和函數是constexpr。

            你要記住的事
  • constexpr對象是const,對它進行初始化的值需要在編譯期知道。
  • 如果使用在編譯期就知道的參數來調用constexpr函數,它就能產生編譯期的結果。
  • 比起non-constexpr對象和函數,constexpr對象很函數能被用在更廣泛的上下文中。
  • constexpr是對象接口或函數接口的一部分。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章