Modern C++ 筆記 - Language Usability 篇

Tech Notes Mar 7, 2023

Constants

nullptr

有些 C++ 把 NULL0 當成同一個東西,因此會產生一些奇怪的行為。以下例:

char *ch = NULL;

void foo(char*);
void foo(int);

當我們呼叫 foo(ch)foo(int) 會被呼叫,而不是我們預期內的 foo(char*)

因此 C++11 以上,用 nullptr 來分別 null pointer 跟 0。

constexpr

有些東西可以在 compile time 就被優化掉,如 const int day_in_secs = 24 * 60 * 60 之類的。

然而如果要做 arr[const] ,compiler 有時候會靠北這東西不是 constexpr,因此 C++11 以上可以用 constexpr 的 keyword 來讓 compiler 知道。

int len = 10;
// char arr_3[len];          // illegal, not constexpr

const int len_2 = len + 1;
constexpr int len_2_constexpr = 1 + 2 + 3;

// char arr_4[len_2];        // illegal, but ok for most of the compilers
char arr_4[len_2_constexpr]; // char arr_5[len_foo()+5];

// legal

另外 constexpr 也可以作為 function return type,因此理論上可以在 compile time 直接算 recursion。

雖然沒用過,但感覺 leetcode 用得到。

constexpr int fibonacci(const int n) 
{ 
	if(n == 1) return 1;
	if(n == 2) return 1;
	return fibonacci(n-1) + fibonacci(n-2);
}

if constexpr

如果想在 compile-time 直接決定 branch judgement(也就是說在 compile 時就知道這個 if/else 要跑哪條 branch)也可以用 if constexpr

constexpr int MAX = 1000;

if constexpr (MAX > 100)
{
		std::cout << "Very Big" << std::endl;
}

Variables and Initialization

if-switch

有時候我們會想先弄出個 temp variable 再對他用 if-else / switch-case 做判斷,如以下例:


std::vector<int> v = {3, 1, 4, 2, 5};

const int index = std::find(v.begin(), v.end(), 2) - v.begin();
if (index < 2)
{
    std::cout << "2 is in the first half" << std::endl;
}
else if (index == 2)
{
    std::cout << "2 is in the middle" << std::endl;
}
else
{
    std::cout << "2 is in the second half" << std::endl;
}

這段 code 在尋找 2v 的前中還後段。

以上程式碼執行起來沒問題,但 index 不會被宣告在 if 的 scope 內,因此如果你之後還需要用到 index 這個名字就有點不舒服。

C++17 提供以下寫法:

std::vector<int> v = {3, 1, 4, 2, 5};

if (const int index = std::find(v.begin(), v.end(), 2) - v.begin(); index < 2)
{
    std::cout << "2 is in the first half" << std::endl;
}
else if (index == 2)
{
    std::cout << "2 is in the middle" << std::endl;
}
else
{
    std::cout << "2 is in the second half" << std::endl;
}

for 回圈一樣,在分號前可以先 initialize 一個變數,這變數會跟著此 if-else / switch-case block 的結束而被 delete。

Initializer List

class Foo 
{
public:
    int value_a;
    int value_b;
    Foo(int a, int b) : value_a(a), value_b(b) {}
};

int main() 
{
		// before C++11
    int arr[3] = {1, 2, 3};
    Foo foo(1, 2);
    std::vector<int> vec = {1, 2, 3, 4, 5};
		
		return 0;
}

可以發現我們要 initialize Foo 的時候跟 array 或 vector 的 {} 形式不一樣,整個就不太爽了。

因此在 C++11 以後可以使用 std::initializer_list 來達到同樣的效果。如下例:

class Foo
{
public:
    int value_a;
    int value_b;
    Foo(std::initializer_list<int> list) : value_a(*(list.begin())), value_b(*(list.begin() + 1)) {}
};

int main()
{
		// after C++11
    Foo foo = {1, 2};
}

在 function 中同樣也可以使用 std::initializer_list ,如下例:

void goo(std::initializer_list<int> list)
{
		for (auto n: list) 
		{
				std::cout << n << std::endl;
		}
}

int main()
{
		goo({1, 2, 3, 4});
}

Structured Binding

如果你想從一個 function 回傳兩個以上的變數,C++11 有 std::tuple,但要把內容物從一個 tuple 拆出來挺麻煩的。

C++17 提供 Structured Binding 幫你解決:

std::tuple<int, double, std::string> f()
{ 
		return std::make_tuple(1, 2.3, "456");
}
int main() 
{
		auto [x, y, z] = f();
		std::cout << x << ", " << y << ", " << z << std::endl; return 0;
}

Type Inference

auto

跟其他現代語言一樣,C++ 在 C++17 以後也可以自己決定變數的 type:

auto i = 3; // int

C++20 後甚至可以在 function argument 中使用:

int add(auto x, auto y)
{
		return x + y;
}

C++14 以後可以用 auto 當 return type:

auto add(auto x, auto y)
{
		return x + y;
}

decltype

可以取得一個 expression 的 type,用法如下:

decltype(expression)

我們就可以拿他做型別判斷

auto x = 1;

if (std::is_same<decltype(x), int>::value)
		std::cout << "int" << std::endl;
else if (std::is_same<decltype(x), float>::value)
		std::cout << "float" << std::endl;

Control Flow

Range-based for loop

C++11 開始,可以跟其他語言一樣 iterate over container,而不需要自己在那邊 i++

std::vector<int> v = {1, 2, 3, 4};
for (auto e : v)
		std::cout << e << std::endl; // read only
for (auto &e : v)
		std::cout << e << std::endl; // read/write

Tags