1.小字符串对象空间优化,SSO(Small String Optimization):
对于短小字符串,c++ 标准库一般采用栈上分配,加快处理速度。可以使用heaptrack 测试程序的heap使用情况。
sso.cc:
#include <string>
#include <iostream>
int main(){
std::string s{"hello,worldabcd-"};
std::cout << s.size() << '\n';
return 0;
}
make sso && heaptrack ./sso
输出:
heaptrack --analyze "/tmp/heaptrack.sso.1321.gz" | grep -A1 -B1 basic_string
输出:
去掉末尾的字符'-', 重新运行:
make sso && heaptrack ./sso
输出:
重新分析heap 使用情况:
heaptrack --analyze "/tmp/heaptrack.sso.1629.gz" | grep -A1 -B1 basic_string
无输出
上面是g++ 自带的libstdc++ 的情况,clang 的lib++, 搜索一下lib++的实现,libc++ 的短字符串表示, 64 位系统上结构体__long 的大小是24字节,普通string其实是basic_string<char>, 所以value_type 就是char, 所以最栈上buffer 大小是23字节。用heaptrack 检查一下:
clang-sso.cc:
#include <string>
#include <iostream>
int main(){
std::string s{"hello,worldabcd-------+"};
std::cout << s.size() << '\n';
return 0;
}
clang++ -std=c++17 -stdlib=libc++ -nostdinc++ -I ~/workplace/llvm7/include/c++/v1/ -L ~/workplace/llvm7/lib/ -Wl,-rpath,/home/yj/workplace/llvm7/lib/ clang-sso.cc -o clang-sso
heaptrack ./clang-sso
heaptrack --analyze "/tmp/heaptrack.clang-sso.4572.gz"|grep -B1 -A1 basic_string
输出:
去掉末尾的字符'+', 重新运行命令:
heaptrack ./clang-sso
heaptrack --analyze "/tmp/heaptrack.clang-sso.5084.gz"|grep -A1 -B1 basic_string
无输出
libstd++ buffer 大小16,去掉末尾的NUL,最大15字符, lib++ buffer 大小23,去掉末尾的NUL, 最大22 字符.
2.实参依赖的名称查找,ADL(Argument-dependent lookup):
#include <cstdio>
namespace A {
struct X{};
struct Y{};
void f(int){
puts("A::f");
}
void g(X){
puts("A::g");
}
void h(Y) {
puts("A:h");
}
} // namespace A
namespace B {
void f(int i) {
using A::f;
f(i);
}
void g(A::X x) {
A::g(x);
}
void h(A::Y y) {
B::h(y);
}
} // namespace B
int main(){
B::f(1);
h(A::Y{});
B::h(A::Y{});
return 0;
}
输出:
A::f
A:h
Segmentation fault (core dumped)
第一句的输出很正常,显式调用namespace B 的 f, B的 f 内部明确宣布使用A 的f;第二句调用了namespace A 中的h, 这里ADL 起作用了,由于实参类型Y来自namespace A, 所以编译器选择了namespace A 中的h;第三 句由于无限递归,段错误了。
如果替换namespace B 中的 h定义:
void h(A::Y y) {
h(y);
}
编译错误:
c++ 的overload set 有两个同名符号h,无法作出选择。更详细的解释例子
3. 模板元编程, TMP(Template Meta Programming):
这里使用Functional Programming in C++中的例子解释,非常不错的书,要说函数式编程,感觉rust 比 c++ 更适合, rust 的类型系统天生拒绝共享易变状态。
type_utils.h:
#ifndef UTILS_H
#define UTILS_H
// Utility class for debugging deduced types
template <typename T>
class error;
// Utility meta-function for detecting type validity
template <typename ...T>
using void_t = void;
// If you are using an older compiler,
// replace the above definition with the following:
// template<typename... Ts> struct make_void { typedef void type;};
// template<typename... Ts> using void_t = typename make_void<Ts...>::type;
// Meta-function that returns the type of an element
// in an iterable collection
template <typename T>
using contained_type =
std::remove_cv_t<
std::remove_reference_t<
decltype(*std::begin(std::declval<T>()))
>
>;
// Meta-function that returns a type with references stripped
template <typename T>
struct remove_reference {
using type = T;
};
template <typename T>
struct remove_reference<T&> {
using type = T;
};
template <typename T>
struct remove_reference<T&&> {
using type = T;
};
template <typename T>
using remove_reference_t = typename remove_reference<T>::type;
// Meta-function that detects whether a type
// has a nested `value_type` type definition
template < typename C
, typename = void_t<>
>
struct has_value_type : std::false_type {};
template<typename C>
struct has_value_type<C, void_t<typename C::value_type>> : std::true_type {};
template < typename C
, typename = void_t<>
>
struct is_iterable : std::false_type {};
template <typename C>
struct is_iterable
<C, void_t < decltype(*std::begin(std::declval<C>()))
, decltype(std::end(std::declval<C>()))
>>
: std::true_type {};
// Meta function that always returns false
// Useful with static_assert and if-constexpr
template <typename...>
struct false_: std::false_type {};
#endif /* !UTILS_H */
test.cpp:
#include <iostream>
#include <algorithm>
#include <numeric>
#include <vector>
#include <string>
#include <type_traits>
#include "type_utils.h"
// Function that sums all items in an iterable collection
template < typename C
, typename R = contained_type<C>
>
R sum_iterable(const C &collection)
{
std::cout << "This is sum_iterable\n";
return std::accumulate(
begin(collection),
end(collection),
R());
}
// Function that sums all items in a collection
// that has a nested value_type definition
template < typename C
, typename R = typename C::value_type
>
R sum_collection(const C &collection)
{
std::cout << "This is sum_collection\n";
return std::accumulate(
begin(collection),
end(collection),
R());
}
// Function that sums all items in a collection
// which first tries to use C::value_type and then
// contained_type<C>
template <typename C>
auto sum(const C &collection)
{
if constexpr (has_value_type<C>()) {
return sum_collection(collection);
} else if constexpr (is_iterable<C>()) {
return sum_iterable(collection);
} else {
static_assert(false_<C>(), "sum can be called only on collections");
}
}
// Meta-function that checks whether two types
// are identical
template <typename T1, typename T2>
struct is_same: std::false_type {};
template <typename T>
struct is_same<T, T>: std::true_type {};
int main(int argc, char *argv[])
{
// Uncomment this to make the compiler write the
// exact result of the contained_type meta-function
// error<contained_type<std::vector<std::string>>>();
// Asserting that std::vector<std::string> contains
// values of type std::string
static_assert(
is_same<
contained_type<std::vector<std::string>>,
std::string
>(), "Expected the contained_type to return std::string");
// Calling the function sum on a vector of ints.
// The result will be an int
std::vector<int> xs { 1, 2, 3, 4 };
std::cout << sum_iterable(xs) << std::endl;
std::cout << sum_collection(xs) << std::endl;
std::cout << sum(xs) << std::endl;
// Uncomment this to trigger the static_assert in the
// sum function
// sum(1);
return 0;
}
这段代码主要是实现泛型函数sum, 可以求出容器内所有元素的和,或者某个可迭代类型的迭代出来的所有值的和。
template <typename C>
auto sum(const C &collection)
{
if constexpr (has_value_type<C>()) {
return sum_collection(collection);
} else if constexpr (is_iterable<C>()) {
return sum_iterable(collection);
} else {
static_assert(false_<C>(), "sum can be called only on collections");
}
}
对于某个未知类型C, sum 函数首先检查类型C是否拥有成员类型value_type, 对于标准库中的容器,都导出了value_type , 如果没有遵循此惯例,使用is_iterable 检查C 类型是否可迭代, 否则编译报错。整个sum函数的决策都是在编译时作出,TMP 是一种编译时计算的技术。函数has_value_type, is_iterable被称作元函数,跟普通函数不太一样, 普通函数在值上运作, 元函数在类型上运作。元函数主要是用于查询修改类型的状态, 比如contained_type,使用了多阶段转换,萃取元素类型:
使用declval 获取T的临时引用(即使这个集合可能没提供默认构造函数,也可以调用其成员函数)
使用begin 获取指向集合中第一个元素的迭代器(begin 需要其参数类型提供begin成员函数)
用decltype计算,解引用迭代器后的类型, 使用remove_reference_t 去除类型可能包含的引用
使用remove_cv_t,去除类型可能包含的const 修饰
4. 替换失败不是错误,SFINAE(Substitution Failure Is Not An Error):
上面的求和的例子中,就包含SFINAE的使用:
template < typename C
, typename = void_t<>
>
struct has_value_type : std::false_type {};
template<typename C>
struct has_value_type<C, void_t<typename C::value_type>> : std::true_type {};
类模板has_value_type 被重载了两次(分别对应主模板和模板特化), 第一个overload处理C 类型内部没有嵌套value_type 的情况,第二个overload 处理C 类型内部存在嵌套value_type 的情况;当类型C 存在value_type 时,两个模板都匹配,但是特化的模板匹配更好, 选取了第二个可以被评估为true;当类型C 不存在value_type 时,编译器在计算void_t 时出错,导致在实例化第二个模板失败,但是编译器不把错误报告给用户,而是安静地从overload set 中,删除第二个模板, 此时只有第一个模板可用,被评估为false。
继承false_type, true_type的目的是让has_value_type 类型成为编译时函数对象, 此函数对象的返回值时false, true:
namespace std {
template<class T, T v>
struct integral_constant {
static constexpr T value = v;
typedef T value_type;
typedef integral_constant type;
constexpr operator value_type() const noexcept { return value; }
constexpr value_type operator()() const noexcept { return value; }
};
template<bool B>
using bool_constant = integral_constant<bool, B>;
using true_type = bool_constant<true>;
using false_type = bool_constant<false>;
}
5.指向实现的指针模式,PIMPL(Pointer to Implementation):
先看一个简单的widget 普通实现:
#include <iostream>
#include <string>
#include <string_view>
class Widget
{
private:
std::string text;
int width = 0;
int height = 0;
bool visible = true;
void draw()
{
std::cout
<< "widget " << '\n'
<< " visible: " << visible << '\n'
<< " size: " << width << ", " << height << '\n'
<< " text: " << text << '\n';
}
public:
void set_text(std::string_view t)
{
text = t.data();
draw();
}
void resize(int const w, int const h)
{
width = w;
height = h;
draw();
}
void show()
{
visible = true;
draw();
}
void hide()
{
visible = false;
draw();
}
};
int main(){
Widget w;
w.resize(100, 20);
w.set_text("sample");
w.hide();
Widget w2 = w;
w2.show();
Widget w3 = std::move(w2);
w3.hide();
return 0;
}
pimpl 模式主要是为了让class接口与实现的分离,让class 的客户减少对class 实现细节的依赖,降低编译时间,通常使用unique_ptr实现自动内存管理:
widget.h:
#ifndef _Widget_include
#define _Widget_include
#include <memory>
#include <string_view>
class Widget
{
private:
class impl;
std::unique_ptr<impl> pimpl;
public:
Widget();
~Widget();
Widget(Widget && other) noexcept;
Widget& operator=(Widget && other) noexcept;
Widget(const Widget& other);
Widget& operator=(const Widget& other);
void set_text(std::string_view text);
void resize(int const w, int const h);
void show();
void hide();
};
#endif
widget.cc:
#include "widget.h"
#include <iostream>
#include <string>
#include <string_view>
class Widget::impl
{
std::string text;
int width = 0;
int height = 0;
bool visible = true;
void draw()
{
std::cout
<< "widget " << '\n'
<< " visible: " << visible << '\n'
<< " size: " << width << ", " << height << '\n'
<< " text: " << text << '\n';
}
public:
void set_text(std::string_view t)
{
text = t.data();
draw();
}
void resize(int const w, int const h)
{
width = w;
height = h;
draw();
}
void show()
{
visible = true;
draw();
}
void hide()
{
visible = false;
draw();
}
};
Widget::Widget() :
pimpl(std::make_unique<impl>())
{}
Widget::~Widget() = default;
Widget::Widget(Widget &&) noexcept = default;
Widget& Widget::operator=(Widget &&) noexcept = default;
Widget::Widget(const Widget& other)
: pimpl(
std::make_unique<impl>(*other.pimpl))
{}
Widget& Widget::operator=(const Widget& other)
{
if (this != &other)
{
pimpl = std::make_unique<impl>(*other.pimpl);
}
return *this;
}
void Widget::set_text(std::string_view text)
{
pimpl->set_text(text);
}
void Widget::resize(int const w, int const h)
{
pimpl->resize(w, h);
}
void Widget::show()
{
pimpl->show();
}
void Widget::hide()
{
pimpl->hide();
}
pimpl.cc:
#include "widget.h"
int main(){
Widget w;
w.resize(200, 150);
w.set_text("new impl");
w.hide();
Widget w2 = w;
w2.show();
Widget w3 = std::move(w2);
w3.hide();
return 0;
}
g++ -std=c++17 -Wall widget.h widget.cc pimpl.cc -o pimpl
6.CRTP(Curiously Recurring Template Pattern ):
crtp 使用模版模拟类型层级, 不需要虚函数的开销,实现了静态分发。
#include <iostream>
template <typename T>
class Widget
{
T* derived() { return static_cast<T*>(this); }
public:
void draw()
{
derived()->drawLine();
derived()->fillColor();
}
};
class Menu: public Widget<Menu>{
public:
void drawLine(){
std::cout << "drawLine in menu" << '\n';
}
void fillColor(){
std::cout << "fillColor in menu" << '\n';
}
};
class CheckBox: public Widget<CheckBox>{
public:
void drawLine(){
std::cout << "drawLine in checkbox" << '\n';
}
void fillColor(){
std::cout << "fillColor in checkbox" << '\n';
}
};
template<typename T>
void drawWidget(Widget<T>& w) {
w.draw();
}
int main(){
CheckBox c;
Menu m;
drawWidget(c);
drawWidget(m);
return 0;
}
但是使用crtp的类型实际上没有一个共同的祖先,所以不能被收集到一个容器内,克服方法是引入一个抽象基类(ABC),然后crtp 模板继承abc, 这样所有使用这个crtp的类型都去实现了某个共同的接口。
#include <iostream>
#include <vector>
#include <memory>
#include <algorithm>
using namespace std;
class WidgetBase {
public:
virtual void draw() = 0;
virtual ~WidgetBase() = default;
};
template <typename T>
class Widget : public WidgetBase{
T* derived() { return static_cast<T*>(this); }
public:
virtual void draw() override
{
derived()->drawLine();
derived()->fillColor();
}
};
class Menu: public Widget<Menu>{
public:
void drawLine(){
std::cout << "drawLine in menu" << '\n';
}
void fillColor(){
std::cout << "fillColor in menu" << '\n';
}
};
class CheckBox: public Widget<CheckBox>{
public:
void drawLine(){
std::cout << "drawLine in checkbox" << '\n';
}
void fillColor(){
std::cout << "fillColor in checkbox" << '\n';
}
};
template<typename T>
void drawWidget(Widget<T>& w) {
w.draw();
}
int main(){
vector<unique_ptr<WidgetBase>> vs;
for(size_t i = 0; i < 5; i++) {
vs.emplace_back(make_unique<Menu>());
vs.emplace_back(make_unique<CheckBox>());
}
for_each(begin(vs), end(vs), [](auto& e) {
e->draw();
});
return 0;
}
crtp 可以被用来制造多个线程安全的单例类型
#include <iostream>
template <class T>
class SingleBase
{
protected:
SingleBase() = default;
public:
SingleBase(SingleBase const &) = delete;
SingleBase& operator=(SingleBase const&) = delete;
static T& instance()
{
static T single;
return single;
}
};
class SingleOne : public SingleBase<SingleOne>
{
SingleOne() = default;
friend class SingleBase<SingleOne>;
public:
void method() { std::cout << "some method from SingleOne" << '\n'; }
};
class SingleTwo : public SingleBase<SingleTwo>
{
SingleTwo() = default;
friend class SingleBase<SingleTwo>;
public:
void method() { std::cout << "some method from SingleTwo" << '\n'; }
};
int main(){
SingleOne::instance().method();
SingleTwo::instance().method();
return 0;
}
7.EBO(Empty Base Optimization):
#include <cassert>
struct E1 {};
struct E2 {};
struct Foo {
E1 e1;
E2 e2;
int data;
};
struct Bar : public E1, public E2{
int data;
};
int main(){
assert(sizeof(Foo) == sizeof(int)*2);
assert(sizeof(Bar) == sizeof(int));
}
Foo 类型中包含两个空类, c++ 标准规定空类也要分配存储,以保持多个相同空类型的对象的身份,但是把类型E1,E2提升成为父类型, 标准又允许编译器把E1,E2优化掉。但是EBO 能不能开启,要看场景:
#include <cassert>
struct Base {};
struct Derived1 : Base {
int i;
};
struct Derived2 : Base {
Base c;
int i;
};
struct Derived3 : Base {
Derived1 c;
int i;
};
int main()
{
assert(sizeof(Base) == 1);
assert(sizeof(Derived1) == sizeof(int));
assert(sizeof(Derived2) == 2*sizeof(int));
assert(sizeof(Derived3) == 3*sizeof(int));
return 0;
}
Derived1 可以开启,Derived1 的对象中包含一个Base 子对象,不需要区分身份; Derived2 不能开启,Derived2 的对象中包含两个Base 子对象,需要区分身份;Derived3 不能开启,Derived3 的对象中包含三个Base 子对象,需要区分身份。标准库和boost 等有多处使用EBO, 比如compressed_pair
8.RVO,NRVO(Return Value Optimization, Named Return Value Optimization):
主要是对象作为函数返回值时,让编译器找机会, 看看能不能把拷贝构造或者移动构造消除掉, 毕竟c++是一个强调值语义的编程语言,需要编译器配合一下,让值语义编程更高效,这里有详细的解释。
9.RAII(Resource Acquisition Is Initialization):
#include <cstdio>
class File final
{
public:
File(std::FILE* file);
~File();
File(const File& src) = delete;
File& operator=(const File& rhs) = delete;
File(File&& src) noexcept = default;
File& operator=(File&& rhs) noexcept = default;
std::FILE* get() const noexcept;
std::FILE* release() noexcept;
void reset(std::FILE* file = nullptr) noexcept;
private:
std::FILE* mFile;
};
File::File(std::FILE* file) : mFile(file)
{
}
File::~File()
{
reset();
}
std::FILE* File::get() const noexcept
{
return mFile;
}
std::FILE* File::release() noexcept
{
std::FILE* file = mFile;
mFile = nullptr;
return file;
}
void File::reset(std::FILE* file) noexcept
{
if (mFile) {
fclose(mFile);
}
mFile = file;
}
int main()
{
File myFile(fopen("file.txt", "r"));
return 0;
}
raii 在c++ 中无处不在, 安全资源管理必备, 一般资源管理类没有copy语义(除非可共享的资源如share_ptr), 可以有move语义。
10. SOO(Small Object Optimization):
soo 跟 sso 类似, 对于小对象尽量分配在栈上,标准库 std::function 就是一个应用soo 的例子,std::function 需要分配单独的存储,保存有状态的可调用对象的状态,指的是内部有数据成员的函数对象, 或者捕获了变量的lambda expr.
#include <cstdio>
#include <functional>
#include <string>
using namespace std;
class F {
private:
static constexpr int bufsize = 17;
char arr[bufsize];
public:
void operator()(){
puts("call operator");
}
};
int main()
{
F f;
function<void()> fc = f;
fc();
printf("sizeof(F) = %lu\n", sizeof(F));
return 0;
}
使用bufsize=17,编译运行:
heaptrack ./sbo
heaptrack -a heaptrack.sbo.13493.gz | grep -A1 -B1 '[[:digit:]]\+B peak'
使用bufsize=16,编译运行:
heaptrack ./sbo
heaptrack -a heaptrack.sbo.13493.gz | grep -A1 -B1 '[[:digit:]]\+B peak'
无输出
clang 的lib++ 实现的std::function 也使用了soo 或者叫 sbo ,但是阀值跟libstd++ 不同,lib++ soo 实现
使用bufsize=25,编译运行:
heaptrack ./clang-sbo
heaptrack -a heaptrack.clang-sbo.15561.gz |grep -A1 -B1 '[[:digit:]]\+B peak'
使用bufsize=24,编译运行:
heaptrack ./clang-sbo
heaptrack -a heaptrack.clang-sbo.15734.gz |grep -A1 -B1 '[[:digit:]]\+B peak'
无输出