C++ 17 新增的 if constexpr 述句能讓我們用更簡潔的方式編寫樣版特化(Template Specialization)。本文會介紹它的語法與應用。
語法
if constexpr 述句的語法與一般的 if 述句相似。兩者的差異在於 if
關鍵字與刮號之間多了一個 constexpr
關鍵字:
if constexpr ( condition ) then-statement
if constexpr ( condition ) then-statement else else-statement
其中,condition 必須是編譯期能確定數值的 constexpr 且該數值必須能被轉型為 bool
。constexpr 包含以下常用的表達式:
<type_traits>
標頭檔定義的 Type Traits:std::is_integral_v<T>
std::is_array_v<T>
std::is_reference_v<T>
std::is_lvalue_reference_v<T>
std::is_rvalue_reference_v<T>
std::is_same_v<U, V>
- 等等
sizeof(TYPE)
與sizeof(NAME)
sizeof...(TYPES)
- constexpr 常數
- 滿足特定條件的 constexpr 函式呼叫(被呼叫者是正確的 constexpr 函式且實際參數都是 constexpr 數值)
當編譯器在處理 if constexpr 述句的時候,編譯器會先計算 condition 的數值。若 condition 為 true
,編譯器會具現化(Instantiate)then-statement,捨棄 else-statement。反之,則會具現化 else-statement,捨棄 then-statement。編譯器只會為被具現化的子句生成目的碼。然而,不論是否被捨棄,編譯器都會檢查程式碼是否有誤,這必須特別留意。
取代類別樣版特化
if constexpr 的第一個用途是取代類別樣版特化(Class Template Specialization)。舉例來說,std::next(iter, n)
能將迭代器 iter
向前移動 n
次。如果傳入的迭代器是隨機存取迭代器(Random Access Iterator),我們能直接回傳 iter + n
。直接使用 operator+
的效率會比較好。然而,如果傳入的迭代器只是向前迭代器(Forward Iterator),我們就只能執行 n
次 ++iter
。
在 C++ 98,這通常以類別樣版特化來實現:
#include <iterator>
namespace impl {
template <typename Category>
class Helper {
public:
template <typename Iterator>
static Iterator next(Iterator iter, size_t n) {
for (size_t i = 0; i < n; ++i) {
++iter;
}
return iter;
}
};
template <>
class Helper<std::random_access_iterator_tag> {
public:
template <typename Iterator>
static Iterator next(Iterator iter, size_t n) {
return iter + n;
}
};
} // namespace impl
template <typename Iterator>
Iterator next(Iterator iter, size_t n) {
typedef typename std::iterator_traits<Iterator>::iterator_category
Category;
return impl::Helper<Category>::next(iter, n);
}
以上程式碼我們先宣告一個以迭代器分類(Iterator Category)為參數的類別樣版 Helper
,再為 std::random_access_iterator_tag
編寫一個類別樣版特化。最後,在 next()
函式的實作中,先以 std::iterator_traits<Iterator>::iterator_category
取得迭代器分類,再呼叫 impl::Helper<Category>::next()
函式。
在 C++ 17,我們能以 if constexpr 改寫上面的類別樣版特化:
#include <iterator>
#include <type_traits>
template <typename Iterator>
Iterator next(Iterator iter, size_t n) {
using Category =
typename std::iterator_traits<Iterator>::iterator_category;
if constexpr (
std::is_same_v<Category, std::random_access_iterator_tag>) {
return iter + n;
} else {
for (size_t i = 0; i < n; ++i) {
++iter;
}
return iter;
}
}
以上程式碼我們還是以 std::iterator_traits<Iterator>::iterator_category
取得迭代器分類。但是接著改以 if constexpr 與 std::is_same_v()
判斷 Iterator
是否為隨機存取迭代器。如果是隨機存取迭代器就直接回傳 iter + n
。因為 if constexpr 會捨棄無關的子句,所以當 Iterator
不是隨機存取迭代器時,iter + n
不會造成編譯錯誤。
備註:作為綀習,你可以刪除 constexpr
關鍵字,觀察編譯器的錯誤訊息,並比較「if 述句」與「if constexpr 述句」的差異。
我們能以下面的程式碼驗證 C++ 98 與 C++ 17 實作的正確性:
#include <iostream>
#include <vector>
#include <list>
int main() {
std::vector<int> xs;
std::list<int> ys;
for (size_t i = 0; i < 5; ++i) {
xs.push_back(i);
ys.push_back(i);
}
std::cout << *next(xs.begin(), 2) << std::endl;
std::cout << *next(ys.begin(), 2) << std::endl;
}
取代樣版函式重載與 std::enable_if
if constexpr 也可以用來取代「函式重載」與「std::enable_if
」。在 C++ 11,一個常用的泛型程式設計技巧是以 std::enable_if
啟用或禁用特定函式重載[1]。舉例來說,上一節介紹的 next
類別樣版也能以 std::enable_if
實作:
#include <iterator>
#include <type_traits>
template <typename Iterator>
typename std::enable_if<
std::is_same<
typename std::iterator_traits<Iterator>::iterator_category,
std::random_access_iterator_tag
>::value,
Iterator>::type
next(Iterator iter, size_t n) {
return iter + n;
}
template <typename Iterator>
typename std::enable_if<
!std::is_same<
typename std::iterator_traits<Iterator>::iterator_category,
std::random_access_iterator_tag
>::value,
Iterator>::type
next(Iterator iter, size_t n) {
for (size_t i = 0; i < n; ++i) {
++iter;
}
return iter;
}
這個技巧是利用 SFINAE(Substitution failure is not an error,代換失敗不是錯誤)挑選重載函式。std::enable_if
的定義如下:
template <bool Condition, typename Type = void>
class enable_if {};
template <typename Type>
class enable_if<true, Type> {
public:
typedef Type type;
};
如果 Condition
為 false
,enable_if
不會有 type
成員,因此會造成代換失敗。C++ 編譯器只會將該重載函式自候選清單移除,不會將該程式碼視為錯誤。上面的 next
重載函式就是透過 std::is_same<typename std::iterator_traits<Iterator>::iterator_category, std::random_access_iterator_tag>::value
選擇不同的實作。
對照上一節的 C++ 17 實作(重複如下),我們能觀察到使用 if constexpr 的版本更為簡潔。我們不需要寫兩次 std::is_same<...>
也不需要解讀一長串晦澀的 std::enable_if
:
#include <iterator>
#include <type_traits>
template <typename Iterator>
Iterator next(Iterator iter, size_t n) {
using Category =
typename std::iterator_traits<Iterator>::iterator_category;
if constexpr (
std::is_same_v<Category, std::random_access_iterator_tag>) {
return iter + n;
} else {
for (size_t i = 0; i < n; ++i) {
++iter;
}
return iter;
}
}
另一個例子
部分 C++ 結構(Struct)沒有建構式,所以當我們要以 new 表達式配置記憶體並初始化成員的時候,我們必須使用大刮號而不是小刮號:
struct PODPoint {
int x_;
int y_;
};
int main() {
PODPoint *u = new PODPoint{1, 2};
// XXX: The following line doesn't compile:
// PODPoint *v = new PODPoint(1, 2);
}
假設我們想要以 unique_ptr
改寫 main()
函式,我們或許會嘗試:
#include <memory>
struct PODPoint {
int x_;
int y_;
};
int main() {
// XXX: The following line doesn't compile:
auto u = std::make_unique<PODPoint>(1, 2);
}
然而在 C++ 17 上面的程式碼是有問題的,因為 std::make_unique<T>(Args&&... args)
只會以 new T(std::forward<Args>(args)...)
配置與初始化物件。
如果我們要自己編寫一個 generalized_make_unique
同時支援兩種 new 表達式,我們能以 std::enable_if
加上 is_constructible_v
實作:
#include <memory>
#include <type_traits>
template <typename T, typename... Args>
typename std::enable_if<
std::is_constructible_v<T, Args...>,
std::unique_ptr<T>>::type
generalized_make_unique(Args&&... args) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
template <typename T, typename... Args>
typename std::enable_if<
!std::is_constructible_v<T, Args...>,
std::unique_ptr<T>>::type
generalized_make_unique(Args&&... args) {
return std::unique_ptr<T>(new T{std::forward<Args>(args)...});
}
如果以 if constexpr 改寫,則可以簡化為:
#include <memory>
#include <type_traits>
template <typename T, typename... Args>
std::unique_ptr<T> generalized_make_unique(Args&&... args) {
if constexpr (std::is_constructible_v<T, Args...>) {
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
} else {
return std::unique_ptr<T>(new T{std::forward<Args>(args)...});
}
}
我們能以下面的程式碼測試兩種實作:
#include <iostream>
struct PODPoint {
int x_;
int y_;
};
struct Point {
int x_;
int y_;
Point(int x, int y) : x_(x), y_(y) {}
};
int main() {
auto u = generalized_make_unique<PODPoint>(1, 2);
auto v = generalized_make_unique<Point>(1, 2);
std::cout << u->x_ << " " << u->y_ << std::endl;
std::cout << v->x_ << " " << v->y_ << std::endl;
}
與回傳型別推導的交互作用
前面的程式範例都明確地指定了函式回傳型別,所以 if constexpr 述句的子句回傳的值都會被轉型為回傳型別。然而,如果我們以 auto
關鍵字讓編譯器幫我們推導回傳型別,則各個 if constexpr 子句不需回傳相同的型別。編譯器在推導回傳型別的時候會忽略被捨棄的子句:
#include <iostream>
template <int Case>
auto test() {
if constexpr (Case == 0) {
return 1.0;
} else if constexpr (Case == 1) {
return 42u;
} else {
return "hello world";
}
}
int main() {
std::cout << test<0>() << std::endl;
std::cout << test<1>() << std::endl;
std::cout << test<2>() << std::endl;
}
以上程式碼之中,不同的樣版參數會讓 test()
有不同的回傳型別:
函式呼叫 | 回傳型別 |
---|---|
test<0>() |
double |
test<1>() |
unsigned int |
test<2>() |
const char * |
不正確的程式碼與編譯錯誤
最後我想要簡單的談一下「不正確的程式碼(Ill-formed Program)」與「編譯錯誤訊息(Diagnostics)」。雖然 if constexpr 述句會依照 condition 的數值決定要具現化哪一個子句,但是編譯器還是會施行許多正確性檢查(不論該子句有沒有被具現化)。例如以下程式碼,雖然 condition 為 false
但是編譯器仍會回報錯誤:
template <typename T>
void example(int i) {
if constexpr (false) {
static_assert(false, "error"); // error 1: assertion failure
char buf[-1]; // error 2: bad array size
i = j; // error 3: undeclared name j
}
}
一些錯誤則會被推遲到真正被使用到的時候,因為只有在具現化之後編譯器才知道各個參數的型別:
struct BadStruct {
int x;
};
template <typename T>
int example(T t) {
if constexpr (sizeof(T) >= sizeof(int)) {
return t.value; // Depending on T, this may be correct or incorrect
}
return 0;
}
int main() {
example(BadStruct{1});
}
然而有一些錯誤雖然被 C++ 標準視為「不正確的程式碼」,但是 C++ 標準允許編譯器自行決定要不要為「沒被具現化的程式碼」產生錯誤訊息(Ill-formed, no diagnostic required):
#include <type_traits>
template <typename T>
void example(T t) {
if constexpr (!std::is_same_v<T, T>) {
char buf[sizeof(T) - sizeof(T) - 1]; // Ill-formed, bad array size
}
}
int main() {
example(1);
}
以上是一段不正確的程式碼,但因為有問題的程式碼沒有被具現化,GCC 和 Clang 都不會產生錯誤訊息。會產生錯誤訊息的編譯器也是合於 C++ 標準的,所以我們仍要確實測試所有 if constexpr 的子句並且避免寫出 Ill-formed 的程式碼。
參考資料
- N4659: Working Draft, Standard for Programming Language C++ (C++ 17 Draft); Section 9.4.1, The if statement; Section 17.6, Name resolution, Clause 8
- P0292R2, P0128R1, P0128R0: constexpr if: A slightly different syntax
- N3638: Return type deduction for normal functions
- N1720: Proposal to Add Static Assertions to the Core Language (Revision 3)
[1] | 雖然 std::enable_if 是在 C++ 11 之後加入標準函式庫,但是它完全能以 C++ 98 實現。如果你使用的是 C++ 98,你可以改用本文提供的 enable_if 實作或是使用 Boost 函式庫的 boost::enable_if 。兩者的原理是一樣的。 |
Note
如果你喜歡這篇文章,請追蹤羅根學習筆記粉絲專頁。最新文章都會在粉絲專頁發佈通知。