首页 C++杂记 - 6
文章
取消

C++杂记 - 6

std::has_unique_object_representations

这个东西用于判断是否有一个唯一性的字节表示。啥意思呢?比如我们某个结构体,可能因为不同的内存对齐要求而插入padding。但是这个8字节对齐的结构体和4字节对齐的结构体的实际含义相同,但是底层的二进制表示却不同。这时候我们就说这个对象的字节表示不唯一。说白了,只要是模糊地带,比如:虚函数,填充位,布尔值,架构,编译器相关的这种东西都会导致它返回false。那么它能干什么?他能干的事情主要是帮助我们进行序列化或让我们判断是否可以直接memcpy

std::monostate

这个之前居然一直忘了写。

1
struct monostate { };

他的定义就这么简单,啥都没有。为啥有这个?

有意作为 std::variant 中的行为良好的空可选项的单位类型。具体而言,非可默认构造的变体类型可以把 std::monostate 列为其首个可选项:这使得此变体自身可默认构造。

对的 就是如果variant里面的类型,第一个为非可默认构造的,那么就用monostate放在前面占位。

1
2
3
4
5
6
7
8
9
10
11
struct s{};

struct t{
    t(int) {}
};

int main(){
    std::variant<s, t> v{};
    std::variant<t, s> v2 {}; // 不行,t不可默认构造
    std::variant<std::monostate, t, s> v1 {};  // OK
}

有人问为啥不直接给s放到t前面。主要是有些场合,t在s构造或有值前是无意义的。他不应该为variant的默认型别。

除此之外还能干什么?

首先注意一下,他并不是类型。但从严格意义上来讲,它同 void 一样,是一个单值类型。C++ 并不像 Rust、Haskell、Scala 等语言那样存在真正的“空”类型。真正的“空”类型无法表示任何值,也无法被创建,所以一个以“空”类型作为参数的函数永远也无法被调用。

std::monostate 类型可以被创建,但该类型只能够包含一个值,因此它也是一个 Unit Type。单值类型只能表示一个状态,这种特殊性使其成为一个没有意义的类型。std::nullptr_t 也是这样的一种类型,不过因为它能够隐式转换成任意的指针类型,并且可以被流输出,误用的几率较大,所以不适合用来作为通用的无意义表示类型。

std::monostate 支持默认构造、拷贝构造和所有的比较操作,是一个常规类型,而 void 是由关键字声明的内置类型,它是一个不完整类型(Incomplete Type),无法创建对象。因此,虽然都是 Unit Type,但 std::monostate 存在一些特殊的使用场景:

第一,你可以用它来测试模板容器的健壮性。因为 std::monostate 不支持输出、运算、隐式转换等功能,也不包含任何成员,是最简单的常规类型,所以可以尝试以其作为模板参数实现化容器,如果没有报错,就表示容器的设计合理,没有强依赖。

第二,你可以将它作为可选模板参数的默认类型。

1
2
3
4
5
template<typename ExtraInfo = std::monostate>
class Data {
    // ...
    ExtraInfo info;
};

用户可以选择是否传递额外的信息,std::monostate 作为默认类型,不会误用、不占空间、也不会和其他类型产生冲突。

第三,你可以将它作为无意义成员的替代类型。

1
2
3
4
5
6
7
8
9
10
11
12
template<bool Debug>
class S {
public:
    void log_info(const std::string& msg) {
        if constexpr (Debug) {
            log_.info(msg);
        }
    }

private:
    std::conditional_t<Debug, Log, std::monostate> log_;
};

这样,在非调试状态下,代码依旧可以正常编译,却不会具备真实的功能。

第四,作为线性递归的 Root 类型。在 C++ Generative Metaprogramming 一书的第 6.3.1 小节,单独写了一个 struct empty_type{} 作为线性递归继承的默认结束条件,那是为了通用性考虑。如果在 C++17 之后也有类似的需求,那么便可以直接使用已经存在的 std::monostate 类型。

命名空间

命名空间有三种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Named Namespaces
namespace A {}                        // C++11
inline namespace B {}                 // C++11
inline namespace [[deprecated]] C {}  // C++17

// Unnamed Namespaces
namespace {}                          // C++11
inline namespace {}                   // C++11
inline namespace [[deprecated]] {}    // C++17

// Nested Namespaces
namespace A::B {}                     // C++17
namespace A::inline B::C {}           // C++17
namespace A::B::inline C::inline D {} // C++20

还有一种特殊的形式称为 Global Namespaces,无须声明,也不属于 Unnamed Namespaces,以 :: 显式访问。通过不需要使用,某些情况下可能在不同的作用域间产生了命名冲突,通过 :: 可以明确指定访问的是最外层的实体,从而避免名称碰撞。

着重讲一下匿名命名空间 Unnamed Namespaces

Unnamed namespaces,也叫 Anonymous namespaces,是命名空间三种形式的一种。这种形式可以省略命名空间的名称,如:

1
namespace { /* .. . */ }

在语义上与等价于:

1
2
namespace unique_name { /* ... */ }
using namespace unique_name;

编译器会自动生成一个唯一的名称,并使用 using-directives 自动导入名称。

与其他形式的命名空间不同,Unnamed namespaces 的链接方式是 Internal Linkage,标准描述为:

An unnamed namespace or a namespace declared directly or indirectly within an unnamed namespace has internal linkage. All other namespaces have external linkage.

注意,Unnamed namespaces 里面的所有内容都是 Internal Linkage,即便是用 extern

Global static 的作用也是 Internal Linkage,Unnamed namespaces 可以作为它的一种替代方式。主要是 static 所赋予的意义太多,有十几种用法,而 Unnamed namespaces 是一种专门用于控制全局可见性的方式。

Unnamed namespaces 当然也有其他问题,比如无法在空间之外特化模板。所以我们有 inline namespace (参考杂记2)

但是在头文件中使用有隐患。因为可能会违背ODR。

using directive 和 using declaration

using-directive(导入整个命名空间)

1
using namespace std;      // using-directive

就是我们常见的导入命名空间。

using-declaration(导入特定名字/重用声明)

1
2
using std::vector;       // 引入单个名字
using std::swap;         // 可以引入一组同名重载

这个就是我们只把某个名字(或该名字的一组重载)引入当前作用域。它的几个扩展用法包括:

  1. 构造函数继承: using Base::Base;
  2. 别名模板/类型别名(C++11): template<class T> using Vec = std::vector<T>; / using i32 = int;

注意要点:

如果同时使用 using-directiveusing-declaration 引入了一个符号,后者的优先级高于前者。

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace A {
    int x = 10;
}

namespace B {
    int x = 20;
}

int main() {
    using namespace A;
    using B::x;
    std::cout << x; // 20
}

使用技巧

本节梳理 C++ Core Guidelines 中的 Namespaces 最佳实践。

  1. 把helper function和作用的类放在同一命名空间。这里的helper function包括operator overload
  2. 除非迫不得已,绝不要在头文件或全局范围中使用using-directive引入整个命名空间
  3. 不要在头文件中使用匿名命名空间。优先使用它来管理不需要导出的内部实体
  4. 不要在头文件的命名空间作用域中使用命名空间别名,除非是在明确标记为仅供内部使用的命名空间中

资料来自

本文由作者按照 CC BY 4.0 进行授权