热门问题
时间线
聊天
视角

C++20

2020版C++編程語言標準 来自维基百科,自由的百科全书

Remove ads

C++20,是继C++17之后的C++编程语言的ISO/IEC标准修订版的名称。[1]2020年2月,该标准在布拉格的会议上由WG21进行了技术定稿[2]。同年9月4日草案获得批准后,C++20同年12月正式發布。[3]相比 C++17,C++20引入了新的语言特性,如概念、模块、操作符“<=>”、协程、指定初始化、新标准属性等。C++20库标准还加入了范围、特性测试宏和位操作等。

特性改动

新增语言特性

功能特性测试

自C++11之后标准引入了大量的C++语言、库特性,在20标准前为了区分这些特性是否生效只能判断C++标准。20标准为这些语言和程序库的功能特性定义了一组预处理器宏,使之成为检测这些功能特性是否存在的一种简单且可移植的方式。测试宏展开会得到该语言、库特性添加到标准草案中的年份和月份,如果该特性有显著变更,宏展开的时间也为更新。

  • 属性测试宏
__has_cpp_attribute( 属性记号 )

宏函数,用以检测属性是否支持,如:

#if __has_cpp_attribute(nodiscard) > 201603L
#pragma message("nodiscard version is c++20")
#endif
  • 语言特性测试宏

用以检测当前某个语言功能特性是否支持,单个宏,如:

#if __cpp_concepts >= 201907L
#pragma message("support concepts")
#endif
  • 标准库特性测试宏

用以检测当前某个标准库特性是否支持,单个宏,不由编译器预定义,由<version>头文件定义:

#ifdef __cpp_lib_bitops
#pragma message("support bitops")
#endif

三路比较和比较操作符的默认

新增三路比较运算符,又称spaceship operator,其形式为:

左操作数 <=> 右操作数

表达式返回一个对象,使得

  • 如果 a < b,那么 (a <=> b) < 0
  • 如果 a > b,那么 (a <=> b) > 0
  • 而如果 a 和 b 相等/等价,那么 (a <=> b) == 0。

三路比较操作符会作为< <= > >=四个操作符的重写候选,若决议选择了带参数顺序的operator<=>,则对于操作如x @ y,执行 x <=> y @ 0,对于不带参数顺序的执行 0 @ x <=> y。

新增可以将比较操作符显式预置=default来要求编译器为某个类生成对应比较,比如:

struct Point
{
    int x;
    int y;
    auto operator<=>(const Point&) const = default;
};
Remove ads

聚合体指派初始化

聚合体初始化的语法糖,在c++11的聚合体初始化基础上,增加了可以指派具体值的语法:

struct U {
  int a;
  float b;
};

U u1{ 1, 2.0 };
U u2{ .a = 1, .b = 2.0 };


范围for中的初始化语句和初始化器

17标准中给if和switch语句加了初始化语句,20标准则给基于范围的for加了初始化语句:

std::initializer_list<int> il{ 1, 2, 3 };
for (size_t index{ 0 }; auto& i : il) {
  std::cout << std::format("index {} value is {}", index++, i) << std::endl;
}

UTF8字符基础类型char8_t

新增了基础类型char8_t用以表示UTF8字符,与11标准中的char16_t、char32_t一样同为语言关键字,char8_t的出现主要是为了和旧有的char区分,专门用于表示utf8字符。相对应的标准库`<string>`增加了std::u8string的别名,以下来自gcc13:

#if __cplusplus >= 201703L && _GLIBCXX_USE_CXX11_ABI
#include <bits/memory_resource.h>
namespace std _GLIBCXX_VISIBILITY(default)
{
_GLIBCXX_BEGIN_NAMESPACE_VERSION
  namespace pmr {
    template<typename _CharT, typename _Traits = char_traits<_CharT>>
      using basic_string = std::basic_string<_CharT, _Traits,
					     polymorphic_allocator<_CharT>>;
    using string    = basic_string<char>;
#ifdef _GLIBCXX_USE_CHAR8_T
    using u8string  = basic_string<char8_t>;
#endif
    using u16string = basic_string<char16_t>;
    using u32string = basic_string<char32_t>;
    using wstring   = basic_string<wchar_t>;
  } // namespace pmr
_GLIBCXX_END_NAMESPACE_VERSION
} // namespace std
#endif // C++17
Remove ads

新属性

no_unique_address

该属性适用于非位域非静态数据成员,指示编译器可以优化当前成员使其与其他非静态数据成员重叠,减少内存占用。如果该成员为空类型(不具有数据成员),则编译器优化为不占空间;如果该成员不为空,则其尾随填充空间可被其他数据成员占用。两个场景例子:

struct Empty {};

struct WithEmpty {
  int32_t x;  // 4字节
  Empty e;  // 1字节(填充至对齐)
};  // 总大小8(4+1+3填充)

struct WithEmptyAttri {
  int32_t x;  // 4字节
  [[no_unique_address]] Empty e;  // 优化为不占空间
};  // 总大小4字节

struct NonEmpty {
  int32_t x;  // 4 字节
  [[no_unique_address]] char y;  // 1字节,无法优化,仍需对齐,*注意这里指定了no_unique_address
  // 3 字节填充
};  // 总大小8字节

struct WithNonEmpty {
  NonEmpty ne;  // 8 字节,未指定no_unique_address,z不可复用ne空间
  char z;  // 1字节, 需对齐
  // 3 字节填充
};  // 总大小12字节

struct Optimized {
  [[no_unique_address]] NonEmpty ne;  // 8 字节
  char z;  // 可以复用 ne 的尾随填充
};

int main() {
  static_assert(sizeof(WithEmpty) == 8);
  static_assert(sizeof(WithEmptyAttri) == 4);
  static_assert(sizeof(NonEmpty) == 8);
  static_assert(sizeof(WithNonEmpty) == 12);
  static_assert(sizeof(Optimized) == 8);
  WithNonEmpty wn;
  assert(&wn.z == &wn.ne.y + 4);
  Optimized o;
  assert(&o.z == &o.ne.y + 1);

  return 0;
}
likely与unlikely

现代cpu有指令预取和分支预测功能,在gcc以往也有__builtin_expect,相对应的20标准新增了likely和unlikely属性作为c++标准的分支预测优化属性,用于给程序员协助编译器完成分支预测。

lambda更新

lambda显式模板形参

可以显式声明模板形参用以表示当前为泛型lambda,如:

auto print = []<typename T>(const T &t) { /*...*/ };
lambda捕获参数包
template <typename... Args>
auto FactoryByValue(Args&&... args) {
  return [... args = std::forward<Args>(args)]() {
    ((std::cout << "Value:" << args << " Address:" << &args << std::endl), ...);
  };
}

template <typename... Args>
auto FactoryByRef(Args&&... args) {
  return [&... args = std::forward<Args>(args)]() {
    ((std::cout << "Value:" << args << " Address:" << &args << std::endl), ...);
  };
}

以上代码中分别按值和按引用捕获了参数包,并通过一元右折叠(c++17功能)打开了参数包。

隐式按值捕获this的弃用

20起须显式声明this的捕获方式,如下代码将被编译器告警:

struct A {
  void Test() {
    auto f = [=]() { cout << this << endl; };
  }
};

typename 关键字简化

模板声明中typename的使用被简化,以下场景中无须再使用typename:

struct Data {
  using ValueType = int32_t;
  using ValuePointer = int32_t*;
};

template <typename T>
struct Foo {
  using Type = T::ValueType;
  typedef T::ValuePointer Pointer;

  T::ValueType val_;

  T::ValueType Get();

  auto Get(size_t index) -> T::ValuePointer;

  template <typename U = T::ValuePointer>
  void Set(U u);
};

consteval与constinit

consteval

新增关键字consteval,只可用于修饰函数,consteval修饰的函数必须是编译期执行:

consteval int sqr(int x) {
  return x * x;
}
constinit

constinit关键字仅可用于变量,表明该变量拥有静态初始化,即该变量的初值为编译期常量,亦即const initialize,仅可用于静态存储期和线程存储期的变量。

const char* g() { return "动态初始化"; }
constexpr const char* f(bool p) { return p ? "常量初始化器" : g(); }
 
constinit const char* c = f(true);     // OK
// constinit const char* d = f(false); // 错误

constexpr 改动

  • constepxr函数可以使用try catch,但异常不允许出现在编译时:
constexpr int Divide(int a, int b) {
    if (b == 0)
        throw std::runtime_error("Division by zero");
    return a / b;
}

constinit int i = Divide(1, 2); // 正常编译期常量
//constinit int j = Divide(1, 0); // 编译报错,编译期不允许出现异常
  • 支持在constexpr函数内使用new、delete动态分配内存,如果是编译期运行则内存不能流出编译期之外:
constexpr int* CreateArray(int size) {
    int* arr = new int[size];
    return arr;
}

constexpr void ReleaseArray(int* p) {
    delete[] p;
}

constexpr int Sum(int n) {
    int* p = CreateArray(n);
    std::iota(p, p + n, 1);  // constexpr since C++20
    auto t = std::accumulate(p, p + n, 0);  // constexpr since C++20
    ReleaseArray(p);
    return t;
}

constinit int i = Sum(10); // 正常编译期计算累计值55

// constinit int* p = CreateArray(100); // 编译出错,编译期内存不能出现在运行期
int* ap = CreateArray(100);  // constexpr函数可以是运行期函数
  • constexpr编译期函数可以是虚函数:
class Parent {
public:
    constexpr virtual ~Parent() = default;

    constexpr virtual int GetID() {
        return 0;
    }
};

class Child : public Parent {
public:
    constexpr virtual ~Child() = default;

    constexpr int GetID() override {
        return 1;
    }
};

constinit int i = Parent().GetID();
constinit int j = Child().GetID();
  • constexpr函数允许修改union活跃成员:
union Test {
    int i;
    int j;
};

constexpr int Foo()
{
    Test t{.i = 1};
    t.j = 2;

    return t.j;
}

constinit int i = Foo();
  • 标准库增加constexpr使用:
#include <vector>
#include <string>
#include <algorithm>

constexpr int Foo() {
  std::vector<int> v;
  v.emplace_back(9);
  v.emplace_back(99);
  v.emplace_back(123);  // vector::emplace_back 20起为constexpr

  std::sort(v.begin(), v.end()); // std::sort 20起为constexpr

  // std::remove_if 20起为constexpr
  const auto erase_start =
    std::remove_if(v.begin(), v.end(), [](auto& i) { return i < 100; } );
  v.erase(erase_start, v.end()); // vector::erase 20起为constexpr

  std::string n = "Joshua";

  return v.size() + n.size(); // 两个size 20起都为constexpr
}

constinit int i = Foo();

有符号整数必须以补码表示

20标准起有符号整数必须以补码表示,N位有符号整数的取值范围也就固定为-(2N-1-1)到2N-1-1。

移位运算符的行为变化

规范了左移、右移操作行为:

  • 左移即是逐位左移并舍弃移出目标类型的位
  • 右移即是算术右移

允许使用圆括号进行聚合初始化

#include <iostream>

struct U {
  int a;
  float b;
};

int main()
{
  U u(1,2);
  std::cout << u.a << " " << u.b << std::endl;
}

缩略函数模板

函数模板可以省略`template<>`声明进行简写:

void f1(auto t) {} // => template<class T> void f1(T);

void f2(const auto& t, auto u) {} // => template<class T, class U> void f2(const T&, U)

void f3(std::integral auto) {} // => template<std::integral T> void f3(T)

void f4(std::integral auto&...) {} // => template<std::integral... Ts> void f4(Ts&...)

数组new 可推导数组长度

int a[]{ 1, 2, 3, 4 };  // 11标准后编译通过
int* pa = new int[]{ 1, 2, 3, 4 }; // 20标准前无法通过编译,20后可以

标准规定在有列表初始化的情况下,可以通过列表个数来确定数组大小。

注释

另见

Loading related searches...

Wikiwand - on

Seamless Wikipedia browsing. On steroids.

Remove ads