专业编程基础技术教程

网站首页 > 基础教程 正文

C++20尝鲜:聚合体初始化变化 c++ 聚合类

ccvgpt 2024-10-10 05:03:21 基础教程 7 ℃

C++20 功能特性

提案

C++20尝鲜:聚合体初始化变化 c++ 聚合类

指派初始化器

P0329R4

括号形式的聚合体初始化

P0960R3

禁止有用户声明构造函数的聚合体

P1008R1

聚合类的类模板实参推导

P1816R0
P2082R1

聚合体

  • 数组类型
  • 符合以下条件的类类型(常为 structunion

没有私有或受保护的直接 (C++17 起)非静态数据成员


没有用户声明的构造函数

(C++11 前)

没有用户提供的构造函数(允许显式预置或弃置的构造函数)

(C++11 起)
(C++17 前)

没有用户提供、继承或 explicit 的构造函数(允许显式预置或弃置的构造函数)

(C++17 起)
(C++20 前)

没有用户声明或继承的构造函数

(C++20 起)

  • 没有虚、私有或受保护 (C++17 起)的基类


没有虚成员函数


  • 没有默认成员初始化器

(C++11 起)
(C++14 前)

聚合体初始化

T 对象 = {实参1, 实参2, ...};

(1)


T 对象 {实参1, 实参2, ... };

(2)

(C++11 起)

T 对象 = { .指派符 = 实参1 , .指派符 { 实参2 } ... };

(3)

(C++20 起)

T 对象 { .指派符 = 实参1 , .指派符 { 实参2 } ... };

(4)

(C++20 起)

T 对象 (实参1, 实参2, ...);

(5)

(C++20 起)

聚合类的类模板实参推导

在C++17中使用带CTAD的聚合,我们需要显式的推导指引,现在没有必要了。

template<typename T, typename U>
struct S{
    T t;
    U u;
};
// 在 C++17,需要推导指引
// template<typename T, typename U>
// S(T, U) -> S<T,U>;
int main(){
  S s{1, 2.0};    // S<int, double>
}

如果有用户提供了推导指引,则不参与CTAD

#include <string>

template<typename T>
struct MyData{
    T data;
};
MyData(const char*) -> MyData<std::string>;

int main(){
    MyData s1{"abc"};   // OK, MyData<std::string> 使用推导指引
    MyData<int> s2{1};  // OK, :显式模板参数
    // MyData s3{1};       // Error, CTAD 不参与
}

可以推导数组类型

#include <cstdio>

template<typename T, std::size_t N>
struct Array{
    T data[N];
};

int main(){
    Array a{{1, 2, 3}}; // Array<int, 3>, 注意额外的括号
    Array str{"hello"}; // Array<char, 6>
}

大括号省略不适用于待决名的非数组类型或待决名边界的数组类型(待决名dependent name)

#include <cstdio>

template<typename T, typename U>
struct Pair{
    T first;
    U second;
};

template<typename T, std::size_t N>
struct A1{
    T data[N];
    T oneMore;
    Pair<T, T> p;
};

template<typename T>
struct A2{
    T data[3];
    T oneMore;
    Pair<int, int> p;
};

int main(){
    // A1::data 是待决名带边界的数组类型 , A1::p  是待决名类型, 大括号省略不支持
    A1 a1{{1,2,3}, 4, {5, 6}};  // A1<int, 3>
    // A2::data 是待决名不带边界的数组类型 , A2::p  不是是待决名类型, 大括号省略支持
    A2 a2{1, 2, 3, 4, 5, 6};    // A2<int>
}

适用于包扩展。一个扩展包的尾随聚合元素对应于所有剩余元素

#include <iostream>

template<typename... Ts>
struct Overload : Ts...{
    using Ts::operator()...;
};
// C++20不需要推到指引

Overload p{[](int){
        std::cout << "called with int" << std::endl;
    }, [](char){
        std::cout << "called with char" << std::endl;
    }
};     // Overload<lambda(int), lambda(char)>
int main(){
    p(1);   //  int
    p('c'); // char
}

无尾随元素包展开对应于没有元素

template<typename T, typename...Ts>
struct Pack : Ts... {
    T x;
};

// 只能推导首元素
int main()
{
  Pack p1{1};         // Pack<int>
  Pack p2{[]{}};      // Pack<lambda()>
  // Pack p3{1, []{}};   // error
}

包中的元素数量只被推导一次,但如果重复,类型应该完全匹配:

#include <tuple>

struct A{};
struct B{};
struct C{};
struct D{
    operator C(){return C{};}
};

template<typename...Ts>
struct P : std::tuple<Ts...>, Ts...{
};


int main(){
    P a {std::tuple<A, B, C>{}, A{}, B{}, C{}}; // P<A, B, C>

    // equivalent to the above, since pack elements were deduced for
    // std::tuple<A, B, C> there's no need to repeat their types
    P b {std::tuple<A, B, C>{}, {}, {}, {}}; // P<A, B, C>

    // since we know the whole P<A, B, C> type after std::tuple initializer, we can
    // omit trailing initializers, elements will be value-initialized as usual
    P c {std::tuple<A, B, C>{}, {}, {}}; // P<A, B, C>

    // error, pack deduced from first initializer is <A, B, C> but got <A, B, D> for
    // the trailing pack, implicit conversions are not considered
    P d {std::tuple<A, B, C>{}, {}, {}, D{}};
}
https://wandbox.org/nojs/gcc-head
https://wandbox.org/nojs/clang-head

禁止使用用户声明的构造函数聚合

现在聚合类型不能有用户声明的构造函数。以前,聚合只允许有删除的或默认的构造函数。这导致了带有默认/删除构造函数的聚合的怪异行为(它们是用户声明的,而不是用户提供的)

// 以下类型在c++ 20中都不是聚合
struct S{
    int x{2};
    S(int) = delete; // 用户声明的构造函数
};

struct X{
    int x;
    X() = default;  // // 用户声明的构造函数
};

struct Y{
    int x;
    Y();            // // 用户声明的构造函数
};

Y::Y() = default;

int  main(){
    S s(1);     // 一直都是错误
    S s2{1};    // C++17正确, C++20错误
    X x{1};     // C++17正确, C++20错误
    Y y{2};     // 一直都是错误
}
https://wandbox.org/permlink/cPA3m3ppHv1h8eIT

圆括号的聚会初始化

圆括号的聚合初始化现在与花括号初始化的工作方式相同,但是允许窄化转换,不允许指定的初始化器,不允许为临时对象延长生命周期,也不允许大括号省略。没有初始化式的元素是值初始化的。这允许无缝使用工厂函数,如std::make_unique<>()/emplace()。

#include <memory>

struct S{
    int a;
    int b = 2;
    struct S2{
        int d;
    } c;
};

struct Ref{
    const int& r;
};

int GetInt(){
    return 21;
}

int  main(){

    //S{0.1}; // error, 花括号不允许窄化(narrowing)
    S(0.1); // OK

    S{.a=1}; // OK
    //S(.a=1); // error, 圆括号不允许指定初始化

    Ref r1{GetInt()}; // OK, 生命周期被延长
    Ref r2(GetInt()); // 危险, 生命周期没被延长

    S{1, 2, 3}; // OK,花括号省略, 相当于 S{1,2,{3}}
    //S(1, 2, 3); // error, 不支持括号省略

    // 没有初始化器的值接受默认值或值初始化(T{})
    S{1}; // {1, 2, 0}
    S(1); // {1, 2, 0}

    // make_unique 正常工作
    auto ps = std::make_unique<S>(1, 2, S::S2{3});

    // 数组也支持了
    int arr1[](1, 2, 3);
    int arr2[2](1); // {1, 0}
}
https://wandbox.org/permlink/w8OrhnuA6WJLb4GA

指派初始化器

每个 指派符 必须指名 T 的一个直接非静态数据成员,而表达式中所用的所有 指派符 必须按照与 T 的数据成员相同的顺序出现。

struct A { int x; int y; int z; };
A a{.y = 2, .x = 1}; // 错误:指派符的顺序不匹配声明顺序
A b{.x = 1, .z = 2}; // OK:b.y 被初始化为 0

指派初始化器所指名的每个直接非静态数据成员,从其指派符后随的对应花括号或等号初始化器初始化。禁止窄化转换。

指派初始化器可用于将联合体初始化为其首个成员之外的状态。只可以为一个联合体提供一个初始化器。

union u { int a; const char* b; };
u f = { .b = "asdf" };         // OK:联合体的活跃成员为 b
u g = { .a = 1, .b = "asdf" }; // 错误:只可提供一个初始化器

对于非联合体的聚合体中未提供指派初始化器的元素,按上述针对初始化器子句的数量少于成员数量时的规则进行初始化(如果提供默认成员初始化器则使用它,否则为空列表初始化):

struct A {
  string a;
  int b = 42;
  int c = -1;
};
A{.c=21}  // 以 {} 初始化 a,这样会调用默认构造函数
          // 然后以 = 42 初始化 b
          // 然后以 = 21 初始化 c

如果以指派初始化器子句初始化的聚合体拥有一个匿名联合体成员,那么对应的指派初始化器必须指名该匿名联合体的其中一个成员。

注意:乱序的指派初始化、嵌套的指派初始化、指派初始化器与常规初始化器的混合,以及数组的指派初始化在 C 编程语言中受支持,但在 C++ 不允许。

struct A { int x, y; };
struct B { struct A a; };
struct A a = {.y = 1, .x = 2}; // 合法 C,非法 C++(乱序)
int arr[3] = {[1] = 5};        // 合法 C,非法 C++(数组)
struct B b = {.a.x = 0};       // 合法 C,非法 C++(嵌套)
struct A a = {.x = 1, 2};      // 合法 C,非法 C++(混合)

最近发表
标签列表