0%

C++ 和 C thread_local 的区别

  1. C++ 支持使用非常量表达式对全局或静态变量初始化 1。对于 static local 2 / thread_local 变量而言,这项功能需要在访问前检查变量是否已经完成初始化,thread_local 初始化不需要线程间同步,而 static local 变量的访问过程需要线程间的同步(__cxa_guard_acquire__cxa_guard_release
  2. C++ 的 thread_local 变量在函数作用域中自动具有 static 属性 3,而 C 要手动加。在 C 语言中,函数中的 thread_local 必须和 extern 或者 static 之一一起使用,例子为 https://godbolt.org/z/eKz71xh7a
  3. C 的 thread_local 在 C23 之前是个宏。

从代价上来看 C++ 的几种变量初始化

  • 首先,不需要函数初始化的在编译期间就能完成工作,没有代价。所以以下讨论的都是通过函数或构造函数来初始化的变量
  • 其次,函数内 (static) thread_local 变量只需要在使用前检查一下,构造和使用都不用同步,代价很小。函数内 static 变量的构造和使用则需要线程之间同步。
  • 函数外定义的普通变量和 thread_local 变量都不需要任何同步就能在静态初始化阶段完成初始化,使用时也不需要检查。
  • 函数外定义的 inline 变量(C++17)在使用时不需要同步,但是在初始化的时候要检查是否已经初始化完成(为此有个 guard variable 标记)。见 https://godbolt.org/z/hYMjdbsxj ,这可能是因为 inline 变量可能被多个地方使用,每个地方都要提防重复初始化。

全局 (和 static) 变量

最外围定义域定义的变量,可以具有 static 属性也可以没有。

不支持非常量初始化表达式(C 模型)

编译器会抱怨:initializer element is not constant。

支持非常量初始化表达式,但只对对象生效(Cfront 1.0 模型)

在用户程序真正运行前插入一段静态变量初始化代码。

Abominable Function Types (open-std.org) 中:

For the purposes of this paper, an abominable function type is the type produced by writing a function type followed by a cv-ref qualifier. Example: using regular = void(); using abominable = void() const volatile &&;

通过一些模板手段能把类中的成员函数转换成可憎函数(怎么翻译更合适?)。可憎函数形式复杂,模板匹配困难。C++23 explicit object parameter (deducing this) 可以一定程度上解决这个问题。

这个东西现在看来不该存在,是 this 指针设计的历史遗留问题。

例子:

#include <iostream>
#include <numeric>
#include <vector>

int main() {
    std::vector v{1, 3, 5}; // 模板类参数推导:T=int
    std::cout << std::accumulate(begin(v), end(v), 0);
    //        ^^                  ^^        ^^ 都是ADL
}

使用 beginendswap 等函数模板时,先引入其名字到当前空间中,然后再调用,有助于 ADL 查找到性能更优的函数实现:

{
    using std::swap;
    swap(A, B); // 假设 A 和 B 是已经定义的同类型变量
}

博文 Should I use the two-step in non-generic code? 认为在非泛型代码中不必使用 using std::swap,直接使用 x.swap(y) 或者 std::swap(如果是内置类型或者 std 名字空间中的类型)即可。

注意,有 bit fields 或者 有成员默认值 或者 有匿名 union 成员 也可以是 aggregate。

对含有 union 成员的聚合类使用 花括号默认的全参数构造函数 初始化时,初始化的是 union 的第一个元素(不管 union 是不是匿名的)。

(C++20) 隐式提供的的全参数构造函数

#include <iostream>
#include <vector>

struct Point {
    double x;
    double y;
};

int main() {
    std::vector<Point> points;
    // 可以省略类型,花括号自动推导。
    points.push_back({1, 1});
    // 必须要写Point类型。因为是模板参数,捕获的时候不知道目的类型,不能推导。
    points.emplace_back(Point{1, 1});
    // 下面两行在 C++20 可以编译:
    points.emplace_back(1, 2);
    auto some_point = Point(1, 2);
}

注意这样的全参数构造函数是具有 explicit 属性的。所以下面的代码不能编译:

用于标注函数返回值

令人意外的是:C++14 加入的返回值推导是一种特殊的类型,和前向声明并不兼容。

int known();
auto known() { return 42; } // 错误:类型不兼容
// int known() { return 42; }         // ok
// auto known() -> int { return 42; } // ok

同样:

auto known();
int known() { return 42; } // 错误:类型不兼容
// auto known() { return 42; } // ok

C++ 14 还加入了 decltype(auto),这个组合不像单独的 auto,不能被 const 等继续修饰。

bash 的两个配置文件

login 时只加载 .bash_profile(如果存在),其他时候只加载 .bashrc

最好是把内容放在 .bashrc,然后让 ~/.bash_profile 去 source ~/.bashrc

虽然 ~/.bashrc 是所有非登陆的 bash 都会读,但默认的 ~/.bashrc 的开头检查了当前是否为交互终端,如果不是则退出。如果非要绕过这个限制,可以强制一个终端为交互终端:bash -i

Windows 的 Cygwin 目录下自己有 home。因此要让它的 ~/.bashrc 去 source Windows 用户目录的 ~/.bashrc,不然会出现两处配置不一致的情况。

bash 显示 git 分支

parse_git_branch() {
    if ! [[ $(git rev-parse --is-bare-repository 2>/dev/null) == true ]]; then
        local BRANCH="$(git branch 2>/dev/null | sed -e '/^[^*]/d' -e 's/* \(.*\)/\1/')"
        [[ "${BRANCH}" == "" ]] && return 0
        local MSG="$(git status 2> /dev/null | tail -n1)"
        if [[ "$MSG" != "nothing to commit"* ]]; then
            echo -n "(${BRANCH}*) "
        else
            echo -n "(${BRANCH}) "
        fi
    fi
}
EXITCODE='$(code=$?; if (( code )); then echo -ne "\[\e[91m\][$code]\[\e[00m\] "; fi)'
export PS1="${EXITCODE}\u \[\e[32m\]\w \[\e[00m\]\[\e[91m\]\$(parse_git_branch)\[\e[00m\]$ "

C/C++ 中全局定义/声明的区别

无论在 C 还是 C++,想要仅声明而不在翻译单元中定义全局变量,都需要 extern。

C语言:(强弱是方便我叙述引出的概念)

int global = 1; // 强定义,和其他强定义互斥
int global;     // 弱定义,可以和同文件其他定义兼容(最终只保留一份定义)
int global;

能编译!但是上面的代码和其他目标文件一起链接时,若出现多次定义同样会引发重复定义的错误。

C++:(单个文件)

四大关键字

  • static_cast 大多数转换,包括左右值转换、父子类指针/引用之间的转换(不进行安全检查,但会修正指针偏移)。
  • const_cast 也能进行 volatile 属性的修改!!
  • dynamic_cast 父子类指针/引用之间的转换。其中子类转向基类相当于使用 static_cast ,没有运行时安全检查。而基类转向子类则有运行时安全检查,而且要求基类是多态类,否则无法编译。
  • reinterpret_cast

标准库的共享指针转换模板

标准库中还有针对于 std::shared_ptr 的类型转换模板。转换后返回一个共享指针,但其包裹的类型被转换成了对应的类型:

  • std::static_pointer_cast
  • std::const_pointer_cast
  • std::dynamic_pointer_cast

注意:这种转换算作指针的复制,所以共享指针对应的控制块引用计数加一(如果指针非空)。

举例:

std::forward 需要我们提供一个模板参数,它能把同类型(不包括值类别)的参数转发出去。

std::forward_like 则有两个模板参数,第一个参数需要显式提供,第二个参数从实参中推导。它从模板参数中获得转发需要使用的引用类型,并将实参转发出去。这意味着实参和模板参数的类型可以不一致!

std::forward_like 非常适合转发子对象的场合,因为它能让子对象和本身的左右值类型相同。比如在显式对象参数中可以使用。

实际上复制构造函数的插入比较困难,比如在函数返回和传参的时候。由于 C 语言是按位复制(而不是按成员),cfront 会在函数调用前插入返回值变量声明,在函数中使用局部变量运算,并在返回时调用复制构造函数从局部变量复制成员到返回值处。

NRVO (Named Return Value Optimization) 则能省略返回时的复制构造:

关于返回值优化

RVO 的范畴很广,NRVO 是 RVO 的一种特例。