前言
本章介绍 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++
开始支持的版本很神秘)。适用于 struct
和 union
,当然不适用于类,因此算得上一种古老而神秘的语法了。
相关文档可以参考 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 #include <iostream> #include "link.h" #include "link2.h" int main () { std::cout << getLinkName () << std::endl; std::cout << getLink2Name () << std::endl; return 0 ; } #pragma once #include <string> std::string getLinkName () ;#include "link.h" static std::string whoami () { return "link" ; }std::string getLinkName () { return whoami (); }#pragma once #include <string> std::string getLink2Name () ;#include "link2.h" static std::string whoami () { return "link2" ; }std::string getLink2Name () { return whoami (); }#!/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 #include <iostream> #include "link.h" #include "link2.h" int main () { std::cout << getLinkName () << std::endl; std::cout << getLink2Name () << std::endl; return 0 ; } #pragma once #include <string> std::string getLinkName () ;#include "link.h" namespace {std::string whoami () { return "link" ; }} std::string getLinkName () { return whoami (); }#pragma once #include <string> std::string getLink2Name () ;#include "link2.h" namespace {std::string whoami () { return "link2" ; }} std::string getLink2Name () { return whoami (); }#!/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
覆盖,因此我们很少提及这一语法。
直接上示例:
原地构造栈资源 原地构造堆资源 顺带一下 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) << std::endl; 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) 。