0%

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++ 的协程是无栈的,这意味着协程只是计算任务,仅在运行时需要栈,在 suspend 之后就会保存状态并脱离栈。要想要跨线程转移计算任务(比如实现工作窃取池),只需要将离栈协程在另外一个线程上 resume 即可。

#include <coroutine>
#include <iostream>
#include <stdexcept>
#include <thread>

auto switch_to_new_thread(std::jthread& out)
{
    struct awaitable
    {
        std::jthread* p_out;
        bool await_ready() { return false; }
        void await_suspend(std::coroutine_handle<> h)
        {
            std::jthread& out = *p_out;
            if (out.joinable())
                throw std::runtime_error("Output jthread parameter not empty");
            out = std::jthread([h] { h.resume(); });
            // Potential undefined behavior: accessing potentially destroyed *this
            // std::cout << "New thread ID: " << p_out->get_id() << '\n';
            std::cout << "New thread ID: " << out.get_id() << '\n'; // this is OK
        }
        void await_resume() {}
    };
    return awaitable{&out};
}

struct task
{
    struct promise_type
    {
        task get_return_object() { return {}; }
        std::suspend_never initial_suspend() { return {}; }
        std::suspend_never final_suspend() noexcept { return {}; }
        void return_void() {}
        void unhandled_exception() {}
    };
};

task resuming_on_new_thread(std::jthread& out)
{
    std::cout << "Coroutine started on thread: " << std::this_thread::get_id() << '\n';
    co_await switch_to_new_thread(out);
    // awaiter destroyed here
    std::cout << "Coroutine resumed on thread: " << std::this_thread::get_id() << '\n';
}

int main()
{
    std::jthread out;
    resuming_on_new_thread(out);
}

可能结果:

Coroutine started on thread: 139972277602112
New thread ID: 139972267284224
Coroutine resumed on thread: 139972267284224

C 风格枚举和 C++ 新增的 enum class:

enum { ITEM_A1, ITEM_A2 };
enum B { ITEM_B1, ITEM_B2 };
// 从 C 沿用来的语法:
// 1. 底层类型默认为int,但也能手动指定
// 2. 枚举名裸露在外
// 区别:
// C 允许前向声明 enum
// C++ 允许枚举列表为空;允许在表示类型时省略 enum 关键字;虽然枚举名裸露但也能通过限定名访问

// C++ 新增 enum class
enum C { ITEM_C1, ITEM_C2 };
// 类型不再裸露,需要使用限定名访问
// 没有默认底层类型,但是可以手动指定

C++ enum class 禁止了隐式转换,但是用来表示 id 还是不太方便:

#include <type_traits>

enum A: int { ITEM_1 };
enum { ITEM_2 };
enum class B: int { ITEM_3 };

// 即便底层类型相同,实际类型也不同
static_assert(! std::is_same_v<int, A>);
static_assert(sizeof(int) == sizeof(A));
static_assert(sizeof(int) == sizeof(ITEM_2));
// 枚举不是聚合类
static_assert(! std::is_aggregate_v<A>);
static_assert(! std::is_aggregate_v<B>);

int main() {
    [](int){}(ITEM_1);
    [](int){}(ITEM_2);
    
    int b3 = (int)B::ITEM_3;
    // [](int){}(B::ITEM_3); // no conversion from B to int
    // [](B){}(b3);          // no conversion from int to B
    [](B){}(B {b3});
    // [](B){}( {b3} );      // 即便是花括号也不能推导,因为枚举不是类

    // 强类型整数还是用 aggregate 做比较好:
    // 虽然整数到枚举可以使用构造的方式描述,但枚举到整数只能强制转换
    // 作为对比,aggregate 直接访问成员即可
}

一般认为 explicit 是用在单参数构造函数 上防止隐式类型转换的。但如果参数有多个,explicit 关键字也有其他的作用。

explicit 可以阻止 {} 推导

https://stackoverflow.com/a/39122237/

可以参考另一篇文章:{ } Syntax。

struct Foo { Foo(int, int) {} };
struct Bar { explicit Bar(int, int) {} };

Foo f1(1, 1); // ok
Foo f2 {1, 1}; // ok
Foo f3 = {1, 1}; // ok

Bar b1(1, 1); // ok
Bar b2 {1, 1}; // ok
Bar b3 = {1, 1}; // NOT OKAY

Bar many_bars[] = { {1, 1} };     // ERROR explicit阻止了 {1, 1} 被推导成 Bar{1, 1}
Bar many_bars2[] = { Bar{1, 1} }; // okay

阻止“去掉默认值是单参数构造函数”的隐式转换

如果将来某个构造函数除了第一个参数之外的其他参数有了默认值,则会引起和单参数构造函数一样的隐式转换问题。

https://www.cppstories.com/2021/heterogeneous-access-cpp20/

C++ 14 加入了对有序容器的异质查找

用户的工作量很小,对标准库中的类型,只需要加上第三个模板参数:std::less<>(它的默认参数是 void)。std::less<void> 类中申明了 is_transparent 类型,所以可以用于异质查找。

std::map<std::string, int> intMap {
    { "Hello Super Long String", 1 },
    { "Another Longish String", 2 },
    { "This cannot fall into SSO buffer", 3 }
};

std::cout << "Lookup in intMap with by const char*:\\n";
std::cout << intMap.contains("Hello Super Long String") << '\\n';
//
std::map<std::string, int, std::less<>> trIntMap {
    { "Hello Super Long String", 1 },
    { "Another Longish String", 2 },
    {"This cannot fall into SSO buffer", 3 }
};

std::cout << "Lookup in trIntMap by const char*: \\n";
std::cout << trIntMap.contains("Hello Super Long String") << '\\n';

C++ 20 加入了对无序容器的异质查找

需要提供标注 is_transparent 的 hash 算子和等于算子。等于算子一般可以直接用 std::equal_to<>,但 hash 算子常常需要我们自己提供。

struct string_hash {
  using is_transparent = void;
  [[nodiscard]] size_t operator()(const char *txt) const {
    // 对指针的hash只考虑了地址,所以要选用string_view的hash
    return std::hash<std::string_view>{}(txt);
  }
  [[nodiscard]] size_t operator()(std::string_view txt) const {
    return std::hash<std::string_view>{}(txt);
  }
  [[nodiscard]] size_t operator()(const std::string &txt) const {
    // string和string_view的hash方式一样
    // 但用string的特化会减少一次从string到string_view的构造
    return std::hash<std::string>{}(txt);
  }
};

std::unordered_map<std::string, int, string_hash, std::equal_to<>>
      intMapTransparent {
    { "Hello Super Long String", 1 },
    { "Another Longish String", 2 },
    {"This cannot fall into SSO buffer", 3 }
};

bool found = intMapTransparent.contains("Hello Super Long String");
std::cout << "Found: " << std::boolalpha << found << '\\n';

下面的代码中,Bar 能够隐式转换成 Foo。想要重载 Foo 的等于运算符至少有三种方案:

  1. 重载全局的 == 运算符
  2. 重载 Foo 中的 hidden friend,即 friend bool operator==(Foo const &a, Foo const &b)
  3. 重载 bool operator==(Foo const &a) const

第一种方案会污染全局的名字空间,使得 Bar 和 Bar 之间也能通过转换两个参数进行比较;第三种方案允许第二个参数的隐式类型转换,但要求第一个参数必须是 Foo 类型;第二种方案只要求任一个参数为 Foo 类型,使得 ADL 能够参与找到这个函数,另一个参数则可以通过隐式类型转换来得到。

成员函数的劣势是由于查找方式和 this 指针的存在,函数失去了对称性。全局函数的劣势则是没有用上 ADL 的优势,hidden friends 在这种情况比较好。

https://godbolt.org/z/6sxdno3Yh

publicprotectedprivate
共有继承publicprotected不可见
私有继承privateprivate不可见
保护继承protectedprotected不可见

可见继承属性就是对于来自基类的 public 和 protected 成员进行一个取最小权限的操作(定义权限 public > protected > private)。

struct 和 class 的数据定义没有区别。语法上区别是:struct 定义默认继承种类是 public,class 定义默认继承类型是 private,这和基类用 struct 还是 class 声明的无关

继承多个类时,需要给不同的基类分别指定访问限定符。

子类继承父类的 protected 属性/方法之后,可以通过子类的指针去访问;子类也不能通过父类指针访问 protected 的成员。

这段代码是在 Tamper Monkey 的 content.js 中发现的。被浏览器警告应该替换掉这种写法。

bn.addEventListener("DOMNodeInserted", o, s)

替换成

var [adder, remover] = ((bn, o, s) => {
    var helper = { remove : () => {}, add : () => {} }
    var observer = new MutationObserver(function(mutations) {
      mutations.forEach(function(mutation) {
        var nodes = Array.prototype.slice.call(mutation.addedNodes);
        nodes.forEach((node_) => { o(); helper.remove() });
      });
    });
    helper.remove = () => observer.disconnect();
    helper.add = () => observer.observe(bn, { childList: true, subtree: true, };
    return [helper.add, helper.remove];
})(bn, o, s);
adder()
bn.removeEventListener("DOMNodeInserted", o, s)