0%

前一篇還是漏掉一些東西,因此才有這篇的存在。

Delegating constructors

在其他程式語言行之有年的玩意,終於加入C++11了。

1
2
3
4
5
6
class Num {
double v;
public:
Num(int v_) : Num((double)v_) {}
Num(double v_) : v(v_) {}
};

Raw String literals

也是在其他程式語言行之有年的觀念

1
2
3
4
5
6
7
8
9
10
11
#include <iostream>
#include <string>
using namespace std;
int main()
{
string normal_str = "First line.\nSecond line.\nEnd of message.\n";
string raw_str = R"(First line.\nSecond line.\nEnd of message.\n)";
cout << normal_str << endl;
cout << raw_str << endl;
return 0;
}

執行結果是

1
2
3
4
5
First line.
Second line.
End of message.

First line.\nSecond line.\nEnd of message.\n

Raw String中的特殊符號都不會被處理。

static_assert

在C++98的時代,就已經有Boost.StaticAssertLoki library可以在編譯期找出可能的錯誤。這項特色終於被加入語言之中。

1
2
static_assert( constant-expression, string-literal );
static_assert(sizeof(void *) == 4, "64-bit code generation is not supported.");

而沒有這項特色的C語言,就只能使用Stackoverflow的方式來做。

nullptr

原本NULL的定意識

1
2
3
#ifndef NULL
#define NULL 0
#endif

當如果有兩個同名函數

1
2
3
4
void foo(int) {}
void foo(void *) {}
foo(NULL); // Compile error
foo(nullptr); // foo(void *)

不過實際使用上,NULL比nullptr更顯眼,畢竟目前對nullptr做Syntax highlight的editor還不多。

儲存了一堆文件,消化一下。

Default template arguments for function templates

以下這個程式碼,在C++98是不可行,但是C++11可以的。

1
2
3
4
5
6
7
template <typename T = int> void Foo(T t = T())
{
}
Foo(12L); // Foo<long>
Foo(10.1); // Foo<double>
Foo('A'); // Foo<char>
Foo(); // Foo<int>

根據Stackoverflow的說法,可以寫類似這樣的Code>

1
2
3
template <typename Iterator, typename Comp = std::less<Iterator>>
void sort(Iterator beg, Iterator end, Comp c = Comp()) {
}

不過當Function Template在Argument deduction時,可能會有以下的問題。

1
2
3
4
5
6
template <typename B, typename T = int>
void Bar(B b = B(), T t = T()) {}
Bar(10); // Bar<int, int>
Bar(10L); // Bar<long, int>
Bar(10L, 20L); // Bar<long, long>
Bar(); // Compile error, couldn't deduce template parameter ‘B’

同樣的在Function Template overload的時候,也是地雷區。

1
2
3
4
5
template <typename T = int> void Foo(T t = T()) {}
template <typename B, typename T = int> void Foo(B b = B(), T t = T()) {}
Foo(12L); // Compile error, ‘Foo(long int)’ is ambiguous
// void Foo(T) [with T = long int] and void Foo(B, T) [with B = long int; T = int]
Foo(); // Compile OK, choose the first function

C++真是細節有夠多的程式語言。

Explicit conversion operators

在C++98的時候,explicit能做的就只有這樣。

1
2
3
4
5
6
class Test1 {
public:
explicit Test1(int) {}
};
Test1 t1(10);
Test1 t2 = 20; // compile error

而對這樣的處理無能為力。

1
2
3
4
5
6
7
8
9
10
11
12
class Test1 {
public:
explicit Test1(int) {}
};
class Test2 {
int x;
public:
Test2(int i) : x(i) {}
operator Test1() { return Test1(x); }
};
Test2 t1 = 10;
Test1 t2 = t1;

t2被偷偷轉換成Test1類型而無法輕易發現,在C++11之後,增加了這樣的能力。

1
2
3
4
5
6
7
8
9
class Test2 {
int x;
public:
Test2(int i) : x(i) {}
explicit operator Test1() { return Test1(x); }
};
Test2 t1 = 10;
Test1 t2 = (Test1)t1;
Test1 t3 = t1; // Compile error

不過bool算是例外。

1
2
3
4
5
6
7
8
9
10
11
12
class Test3 {
public:
explicit operator bool() { return true; }
};
void Foo()
{
Test3 t3;
if (t3) {
// if constructor's bool conversion is treated as explicit.
}
bool b = t3; // Compile error
}

Initializer lists and Uniform initialization syntax

在C語言可以這樣寫沒問題

1
int a[] = {1, 2, 3};

不過在C++98,用std::vector的時候,就只能寫成這樣。

1
2
3
4
std::vector<int> a;
a.push_back(1);
a.push_back(2);
a.push_back(3);

支援Initializer lists之後,終於可以寫出比較美觀的程式碼

1
std::vector<int> a = {1, 2, 3};

你也在自己的城市中使用Initializer lists,如下:

1
2
3
4
5
6
7
template <typename T>
void print(const initializer_list<T>& nums)
{
for (auto num : nums)
cout << num << endl;
}
print({1, 2, 3, 4});

既然有了Initializer lists之後,C++11更進一步將Initialization簡化,使用同樣的語法來做初始化。
Case 1:

1
2
3
4
5
6
7
struct C
{
int a;
int b;
C(int i, int j) {}
};
C c {0,0}; //C++11 only. Equivalent to: C c(0,0);

Case 2

1
int* a = new int[3] { 1, 2, 0 }; //C++11 only 

Case 3

1
2
3
4
struct X {
int a[4];
X() : a{1,2,3,4} {} //C++11, member array initializer
};

Case 4:

1
2
3
4
struct C
{
int a = 7; //C++11 only
};

更進一步,將Initializer list和 Uniform initialization雙見合併。

1
2
map<string, string> singers ={ {"Lady Gaga", "+1 (212) 555-7890"},
{"Beyonce Knowles", "+1 (212) 555-0987"}};

這樣的程式碼易讀許多。

Agner Fog最近更新了他的Software optimization resources
之前在Corel的時候也有參考過這邊的文件。不過離開那邊之後就沒碰過Assembly了。

不過Optimizing software in C++: An optimization guide for Windows, Linux and Mac platforms這篇文件可以看看。
這年頭自己寫Assembly的人越來越少,除了是大師之外,沒有把握比Compiler做的更好。

StackOverflow看到的。
這段程式,在GCC和Clang會失敗,而在VC正常運作。

1
2
3
4
5
int main()
{
int linux = 1;
return 0;
}

gcc -E下去看的結果如下

1
2
3
4
5
int main()
{
int 1 = 1;
return 0;
}

這是由於在未標準化的年代,像是unix,linux等字都會採到地雷。
不過可以用 gcc -std=c89強讓他走標準規範編譯。

可以看到

1
2
3
4
5
6
7
8
9
$ cpp --std=c89 -dM < /dev/null | grep linux
#define __linux 1
#define __linux__ 1
#define __gnu_linux__ 1
$ cpp --std=gnu89 -dM < /dev/null | grep linux
#define __linux 1
#define __linux__ 1
#define __gnu_linux__ 1
#define linux 1

在gnu89裡面GCC自己加了linux的定義下去了。注意這邊的cppThe C Preprocessor,跟C++無關。
-dM來印出所有被Preprocessor定義的值。
既然知道Preprocessor定一些什麼值之後,就可以順利避開陷阱了。

東西閱看越多姿後,才發覺自己的無知。 寫起來當筆記。

First-class citizen

Wiki上有其定義。另一個名稱叫First-class object,其特性有

  • 可以被存入變數或其他結構
  • 可以被作為參數傳遞給其他函數
  • 可以被作為函數的返回值
  • 可以在執行期創造,而無需完全在設計期全部寫出
  • 即使沒有被繫結至某一名稱,也可以存在
    其中最具討論性的是函數算不算First-class citizen,C/C++不支援在Runtime創造函數,所以不把函數視為First-class citizen。

Higher-order unction

[Wiki](http://en.wikipedia.org/wiki/Higher-order_function)上同樣有定義。一個Higher-order unction至少滿足以下條件之一。

  • 接受一個或多個函數作為輸入

Closure

Wiki同樣有其定義。

用在一個函式與一組「私有」變數之間建立關聯關聯。在給定函式被多次呼叫的過程中,這些私有變數能夠保持其永續性。變數的作用域僅限於包含它們的函式,因此無法從其它程式代碼部分進行存取。不過,變數的生存期是可以很長,在一次函式呼叫期間所建立所生成的值在下次函式呼叫時仍然存在。正因為這一特點,閉包可以用來完成訊息隱藏,並進而應用於需要狀態表達的某些編程典範中。

用這種方式來使用閉包時,閉包不再具有參照透明性,因此也不再是純函式。

Tailing Recursion是大部分Function Language的基本配備,在沒有迴圈的情況下,Recursive是唯一的方案。
而Tailing Recursion是Tail Call的一種特例。只是最後呼叫的函數是自己。
在不討綠用公式解跟迴圈解的情況下,要從1加到1000000000我們會這麼寫。

1
2
3
4
5
6
long long sum(int n)
{
if (n == 0) return 0;
return n + sum(n - 1);
}
cout << sum(1000000000) << endl;

這段程式在Visual C++會掛,而在GCC跟Clang可以正常運作。 (Visual C++的判別還真有點弱)。
不過我想說的是後面這個,Tailing Recursion Optimization
跟上面很類似,不過會多帶一個參數,代表初始狀態。
上面的範例可以重新寫成

1
2
3
4
5
6
long long int sum(int n, long long int v)
{
if (n == 0) return v;
return sum(n - 1, n + v);
}
cout << sum(1000000000, 0) << endl;

這種寫法比上面好的點,在於當我們要做Recursive呼叫的時候,必須保留呼叫時每一層的狀態,存在Stack裡,當你遞迴深度過深時,超過OS給予的極限,就會造成Stackoverflow。而用下面這種寫法的話,則不需要保留遞迴中的堆疊。只需要修改當前堆疊的值,然後既需呼叫函式即可。

還有如果把之後要做的事情當做參數傳進來,就變成Continuation-passing style了。

把程式碼改寫成這樣

1
2
3
4
5
6
void sum(int n, long long int v, function<void (long long)> contWith)
{
if (n == 0) return contWith(v);
return sum(n - 1, n + v, contWith);
}
sum(1000000000, 0, [] (long long v) { cout << v << endl; });

結果所有編譯器全掛了,同樣邏輯的程式碼用Haskell重寫一次

1
2
3
sum' n total cont  
| n == 0 = cont(total)
| otherwise = sum' (n - 1) (total + n) cont

整個計算資源被耗盡了,難道1000000000真的玩太大了。

由於Asynchronous Programming大行其道之後,Continuation Passing Style就再度被人們注意到。
在介紹Continuation Passing Style(之後簡稱CPS)之前,先要介紹一下什麼是Continuation。指的是完成某件事情之後,接下來還需要做的情情

而什麼是CPS,就是將Continuation當做參數傳入函數之中。

1
2
3
 
let add a b = a + b
printfn "%i" (add 3 5)

而CPS的寫法會是

1
2
3
 
let add a b cont = cont(a + b)
add 3 5 (printfn "%i")

兩者能夠得到一樣的結果,不過光看這麼簡單的範例,看不出CPS優勢和在。

從C語言說起

在C語言的時候,const的用途很簡單,用來修飾變數的屬性。以下給個範例

1
2
3
4
5
6
void func(const int *v)
{
int p;
*v = 10;
v = &p;
}

上面的*v = 10會被編譯器指出v是不能被修改的。 值得注意的是 const int *的寫法跟 int const *是一樣的,不過我比較偏好前者。

如果改寫成

1
2
3
4
5
6
void func(int * const v)
{
int p;
*v = 10;
v = &p;
}

會告訴你v = &p這行錯了,由此可見int const *表示被指向的 內容 不可改,而指標是可以改變的,這邊的const是用來修飾int的。而int * const表示指向的 指標 不可改,而這邊的const是用來修飾int *的。
當然,如果要兩者間得,也可以寫成這樣

1
2
3
void func(const int * const v)
{
}

C++98

C++擴大const的使用範圍,允許const修飾Class的Member Function。表示這個函數是 Logic Constness,不影響外界看這物件的狀態。因此以下這段程式碼會出現問題。

1
2
3
4
5
6
7
class Test {
int state;
public:
void Func() const {
state = 1;
}
};

由於C++支援Cast Overloading,支援Function Signature相同,但const屬性不同的Overload,因此這樣的是合法的,,

1
2
3
4
5
6
7
8
9
10
class Test {
int state;
public:
void NonConstFunc() {}
void Func() {
state = 0;
}
void Func() const {
}
};

寫個程式來測試一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
oid Test1(const Test &t)
{
t.NonConstFunc(); // Compile Error, 不能呼叫Non-const的Member Function
t.Func();
}
void Test2(Test &t)
{
t.NonConstFunc();
t.Func();
}
void Test3(Test &&t)
{
t.NonConstFunc();
t.Func();
}
Test t;
Test1(t);
Test2(t);
Test3(move(t));

除了Test1的Func是跑const版本之外,其他所有函數都是呼叫Non-const版本的Func
雖說const的Member function是不可修改 Logic Constness ,不過以下程式碼很難是合法的。

1
2
3
4
5
6
7
8
class Test {
int *state;
public:
void NonConstFunc() {}
void Func() const {
*state = 1;
}
};

Mutable

之前說過,從外界看到的類別狀態是 Logic Constness 的,這代表我們可以在 const函數裡面動手腳,只要外界看起來正常就好,因此就有了mutable的誕生。將上面的範例重新改寫。

1
2
3
4
5
6
7
class Test {
mutable int state;
public:
void Func() const {
state = 1;
}
};

這樣就能正常使用了…初看之下好像很沒用,不過以這個範例來說

1
2
3
4
5
6
7
8
9
10
11
12
13
class Test {
int state;
mutable mutex obj_mutex;
public:
void SetState() {
unique_lock<mutex> lock(obj_mutex);
state = 1;
}
int GetState() const {
unique_lock<mutex> lock(obj_mutex);
return state;
}
};

在Multithread的情況之下,有人會呼叫SetState,而有人會想知道GetState的值,雖然state在GetState不會被改變,但是mutex會變。所以需要mutable的存在。
另外一種情形是當做Cache使用

1
2
3
4
5
6
7
8
9
10
11
12
class HashTable {
mutable string lastKey, lastValue;
....
public:
string lookup(string key) const {
if (key == lastKey) return lastValue;
string value = ookupInternal(key);
lastKey = key;
lastValue = value;
return value;
}
};

不過在C++11之後,又有新用法了

1
2
3
4
int x = 30;
auto f1 = [=]() { x = 123; } // Compile error
auto f1 = [=]() mutable { x = 123; }
auto f2 = [&]() mutable { x = 123; }

f1在呼叫之後,x會變成123,不過離開f1之後,x又回到30,而f2呼叫之後就整個變成123了。
不過我看不懂第一個範例的用途是什麼。

constexpr

constexpr是C++11才有的觀念,原先就有Constant Expressions的觀念,不過還是有其不足之處。
假設我們要宣告個n*m的一維陣列,我們會這麼做。

1
2
3
const int n = 5;
const int m = 5;
int array[n * m];

假設我們已經有一個mul2的函數,試著編譯以下這段程式就會出現錯誤。

1
2
3
4
const int n = 5;
const int m = 5;
int mul2(int x, int y) { return x * y; }
int array[n * m];

結果我們只能藉由以下兩種方法解決,一是回到C語言的Preprocessor來做。

1
#define mul2(x, y) ((x) * (y))

這方式可行,不過缺乏型態資料。在Secure Coding的時候容易出錯。
另一種是在Runtime時算出一個常數。

1
2
3
4
5
int mul2(int x, int y) { return x * y; }
const int Mul2 = mul2(n, m);
void func() {
int array[Mul2];
}

如果要把array儀到global scope,問題又出現了。導致不得不使用Preprocessor的解法。因此就有了constexpr的誕生。
有了constexpr之後,可以在編譯時期就算出答案,類似Template Metaprogramming,不過用途更廣。
上面的範例我們可以重新改寫

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <stdio.h>
using namespace std;
const int n = 5;
const int m = 5;
constexpr int mul2(int x, int y) { return x * y; }
int array[mul2(n, m)];
int main() {
int x, y;
scanf("%d %d", &x, &y);
printf("arraySize = %lu\n", sizeof(array) / sizeof(array[0]));
printf("mul2((x, y) = %d\n", mul2(x, y));
return 0;
}

可以看到這邊的mul2不只可以用在compile-time,在runtime也可正常執行。
也可以在編譯時期使用物件,上面的範例我們可以用Functor在寫一次。

1
2
3
4
5
6
7
8
9
10
const int n = 5;
const int m = 5;
class Mul2 {
int x, y;
public:
constexpr Mul2(int x_, int y_) : x(x_), y(y_) {}
constexpr int operator()() { return x * y; }
};
constexpr Mul2 mul2(n, m);
int array[mul2()];

如果對constexpr有更多了解,可以參考Constexpr - Generalized Constant Expressions in C++11,目前支援constexpr的編譯器也不多。

結論

C++果然不愧是最難學的語言,每個環節都搞的特別複雜。我難過。