前言

本章介绍 C/C++ 中一些鲜为人知的语法,这些语法可能不常在系统教学中提及,因此显得格外神秘。

本人能力有限,经常在阅读别人代码时发现一些未曾见过的语法使用,它们通常比较冷门而显得神秘;我希望将这些看到的神秘语法进行记录,也希望对你有所帮助。

理想预期下,这篇会持续更新。文中术语如不确切,希望指正。


指定初始化 Designated Initializers

点击展开

这个语法是我在写某个公开课实验的时候发现的,当时好像是突发奇想为什么 C++ 没有其他语言支持的指定参数之类,然后瞎试靠 IDE 补全发现的。

废话不多,直接上示例吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
#include <iostream>
#include <string>

struct Inner {
int x;
char c;
};

struct Example {
std::string s;
Inner in;
};

void Print(const Example &ex) {
std::cout << "(std::string)ex.s=\"" << ex.s << "\"" << std::endl;
std::cout << "(int)ex.in.x=" << ex.in.x << std::endl;
std::cout << "(char)ex.in.c=\'" << ex.in.c << "\'" << std::endl;
}

int main() {
Example ex{
.s="123",
.in={
.x=1,
.c='c',
},
};
Print(ex);
return 0;
}

指定初始化看起来很好用,从 C99开始支持(C++ 开始支持的版本很神秘)。适用于 structunion,当然不适用于类,因此算得上一种古老而神秘的语法了。

相关文档可以参考 Aggregate initialization Designated initializers - cppreference.com

嗯… 所以为什么 C++ 没有支持指定参数?还是我孤陋寡闻?


匿名命名空间 Anonymous Namespace 和 static 函数

点击展开

之前一直在学校学习,见识不到这些。第一次实习时,在公司代码中看到这些,于是去修修补补。

匿名命名空间(anonymous namespace,我后面更喜欢称之为 namespace {})和 static 函数作用都是提供变量、函数仅文件可见的作用域限定,主要用于避免不同文件间内部变量、函数的命名冲突。其实现细节是编译时的内部链接,这意味着这类实体的定义只能在编译单元内可见。

还是来写例子吧。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
// main.cpp
#include <iostream>
#include "link.h"
#include "link2.h"

int main() {
std::cout << getLinkName() << std::endl;
std::cout << getLink2Name() << std::endl;
return 0;
}

// link.h
#pragma once
#include <string>
std::string getLinkName();

// link.cpp
#include "link.h"
static std::string whoami() { return "link"; }
std::string getLinkName() { return whoami(); }

// link2.h
#pragma once
#include <string>
std::string getLink2Name();

// link2.cpp
#include "link2.h"
static std::string whoami() { return "link2"; }
std::string getLink2Name() { return whoami(); }

// run.sh
#!/usr/bin/sh
g++ main.cpp link.cpp link2.cpp -o main
./main

这里如果将两处 static 去掉是无法编译的。这就是因为函数声明默认是外部链接的,这时就会出现重复定义。通过使用 static 就可以将函数隐藏在内部,从而避免内部函数与外部存在命名冲突的可能。

namspace {} 的功能和 static 类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// main.cpp
#include <iostream>
#include "link.h"
#include "link2.h"

int main() {
std::cout << getLinkName() << std::endl;
std::cout << getLink2Name() << std::endl;
return 0;
}

// link.h
#pragma once
#include <string>
std::string getLinkName();

// link.cpp
#include "link.h"
namespace {
std::string whoami() { return "link"; }
}
std::string getLinkName() { return whoami(); }

// link2.h
#pragma once
#include <string>
std::string getLink2Name();

// link2.cpp
#include "link2.h"
namespace {
std::string whoami() { return "link2"; }
}
std::string getLink2Name() { return whoami(); }

// run.sh
#!/usr/bin/sh
g++ main.cpp link.cpp link2.cpp -o main
./main

好吧,事实上我找不到 namespace {}static 函数的不同。


用户定义字面量 User Defined Literals

点击展开

最近随便捣鼓了一下 pybind11,很好的一个开源项目。结果搞一半开始研究奇怪的东西了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <iostream>
#include <string>

std::string operator""_s(const char *str, size_t len) {
return std::string(str, len);
}

std::string operator""_lw(const char *str, size_t len) {
std::string res(str, len);
for (char &c : res) {
if ('A' <= c and c <= 'Z') {
c += 0x20;
}
}
return res;
}

std::string operator""_up(const char *str, size_t len) {
std::string res(str, len);
for (char &c : res) {
if ('a' <= c and c <= 'z') {
c -= 0x20;
}
}
return res;
}

int main() {
std::cout << "Hello World!"_s << std::endl;
std::cout << "Hello World!"_lw << std::endl;
std::cout << "Hello World!"_up << std::endl;
return 0;
}

用户定义字面量从 C++11 开始支持。用户自定义字面量只支持 const char *, size_t / long double / unsigned long long / char 等参数列表。

相关文档可以参考 User-defined literals (since C++11) - cppreference.com


原地构造 Placement New

点击展开

之前学 allocator(标准库定义的一个类)就在想是怎么实现的,后来查了一些资料是有特别的语法支持。

placement new 最常提及的应用就是 allocator(标准库定义的内存分配器)可以在创建数组时减少构造次数,vector 等标准库容器就是使用 allocator 进行扩容的。由于使用 placement new 的场景几乎可以被 allocator 覆盖,因此我们很少提及这一语法。

直接上示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <iostream>

struct A {
A(int x) noexcept { std::cout << "A(" << x << ")" << std::endl; }
virtual ~A() { std::cout << "~A()" << std::endl; }
};

constexpr uint32_t PIECESIZE = sizeof(A);
constexpr uint32_t CAPACITY = 10;
constexpr uint32_t SIZE = 5;

int main() {
uint8_t capacity[CAPACITY * PIECESIZE];
std::cout << "Start Construct" << std::endl;
for (int i = 0; i < SIZE; i++) {
new (capacity + i * PIECESIZE) A(i);
}
std::cout << "Start Destroy" << std::endl;
for (int i = 0; i < SIZE; i++) {
reinterpret_cast<A *>(capacity + i * PIECESIZE)->~A();
}
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <iostream>

struct A {
A(int x = 0) noexcept { std::cout << "A(" << x << ")" << std::endl; }
virtual ~A() { std::cout << "~A()" << std::endl; }
};

constexpr uint32_t PIECESIZE = sizeof(A);
constexpr uint32_t CAPACITY = 10;
constexpr uint32_t SIZE = 5;

int main() {
auto capacity = new uint8_t[CAPACITY * PIECESIZE];
std::cout << "Start Construct" << std::endl;
for (int i = 0; i < SIZE; i++) {
new (capacity + i * PIECESIZE) A(i);
}
std::cout << "Start Destroy" << std::endl;
for (int i = 0; i < SIZE; i++) {
reinterpret_cast<A *>(capacity + i * PIECESIZE)->~A();
}
delete []capacity;
return 0;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <iostream>
#include <memory>

struct A {
A(int x) noexcept { std::cout << "A(" << x << ")" << std::endl; }
virtual ~A() { std::cout << "~A()" << std::endl; }
};

int main() {
std::allocator<A> alloc;
const int n = 10, m = 5;
A *p = alloc.allocate(n);
for (int i = 0; i < m; i++) {
alloc.construct(p + i, i);
}
for (int i = 0; i < m; i++) {
alloc.destroy(p + i);
}
alloc.deallocate(p, n);
return 0;
}

相关文档可以参考 std::allocator - cppreference.com

相关文档可以参考 new expression Placement new - cppreference.com


类成员指针 Pointer to Member

点击展开

也是在一次和群友讨论中偶然发现的。

这次先看代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
#include <iostream>

struct Bank {
int id_;
double money_;
};

int main() {
double Bank::*money = &Bank::money_;
std::cout << reinterpret_cast<int&>(money)/* 8 */ << std::endl; // 类成员指针实际是一个 offset
Bank bank{1, 1e9};
std::cout << bank.*money << std::endl;
}

第 9 行定义了类成员指针 money,它的类型是 double Bank::*。然后可以通过 bank.*money 来获取 Bank 类型对象的 money_ 成员。

该语法从 C++98 开始支持,C 不支持。额,难怪这种语法神秘,我实在想不到它的应用场景。

相关文档可以参考 Pointer declaration Pointer to member - cppreference.com


黑魔法 - 模板实例化偷私有成员

点击展开

某天群友聊起了黑魔法… 不知道这算不算 bug,但现在应该算一个 feature。

C++ 中外部函数访问私有变量,正确做法是提供 Getter 或者使用友元。

但是有一种黑魔法禁术可以非侵入地访问和修改类的私有成员,只需要知道目标的名字和类型。不依赖内存分布、宏、类型转换。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
#include <iostream>

class Bank {
private:
double money = 999'999'999;

public:
void check() {
std::cout << "check: " << money << "\n";
}
};

template<auto M>
struct Tunnel {};

template<class T, class U, T U::*M>
struct Tunnel<M> {
friend T &sneak(U &u) {
return u.*M;
}
};

template class Tunnel<&Bank::money>;
double &sneak(Bank &);

int main() {
Bank bank;
bank.check();

auto &take_control = sneak(bank);
auto booty = take_control;
take_control = 0.114514;
std::cout << "booty: " << booty << "\n";

bank.check();
}

该魔法的原理就是编译器允许模板实例化时使用私有成员,然而这里巧妙的将模板实例化获取的引用向外抛出。

原文请参考:C++中外部函数如何访问私有变量?(zhihu.com)