0%

const, mutable and constexpr

從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++果然不愧是最難學的語言,每個環節都搞的特別複雜。我難過。