قابلیت‌های سی پلاس پلاس (++C) که باید بدانید — با مثال و به زبان ساده

۲۸۸۷ بازدید
آخرین به‌روزرسانی: ۲۰ شهریور ۱۴۰۲
زمان مطالعه: ۴۷ دقیقه
قابلیت‌های سی پلاس پلاس (++C) که باید بدانید — با مثال و به زبان ساده

اگر آشنایی ابتدایی با زبان برنامه‌نویسی ++C دارید و می‌خواهید با قابلیت های سی پلاس پلاس نسخه 20 یعنی جدیدترین نسخه این زبان بهتر آشنا شوید و خلاصه کوتاهی در مورد هر یک از این قابلیت‌ها در یک راهنمای جامع مطالعه کنید، مطلب خوبی را برای خواندن انتخاب کرده‌اید. این مطلب در واقع یک برگه تقلب در مورد امکانات و قابلیت‌های C++20 است. اگر به تازگی با این زبان برنامه‌نویسی آشنا شدید و یا می‌خواهید این زبان را به روشی اصولی‌تر بیاموزید، پیشنهاد می‌کنیم کار خود را از آموزش زیر آغاز کنید:

فهرست مطالب این نوشته
997696

مفاهیم کلی

در این بخش مواردی که کامپایلر از یک آرگومان تمپلیت نیاز دارد تا پیش از «وهله‌سازی» (instantiation) بررسی کند را توضیح می‌دهیم. در نتیجه این کار در صورت نیاز به نمایش پیام خطا، آن پیام رو‌شن‌تر خواهد بود. برای نمونه می‌توانیم به کاربر اعلام کنیم که شرط X احراز نشده است. تا پیش از C++20 می‌توانستیم از یک میانبر استفاده کرده و از سازه‌های enable_if بهره بگیریم و در غیر این صورت باید در زمان وهله‌سازی با نمایش یک پیام ‌خطای رمزآلود برنامه را متوقف می‌ساختیم. با معرفی کانسپت‌ها، این نقطه شکست بسیار زودتر رخ می‌دهد و لذا پیام خطا روشن‌تر خواهد بود.

عبارت Requires

کار خود را با requires-expression آغاز می‌کنیم.

این عبارت شامل الزامات واقعی آرگومان‌های تمپلیت است و در صورتی که این الزامات برآورده شوند، مقدار true و در غیر این صورت مقدار false بازگشت می‌دهد.

1template<typename T> /*...*/
2requires (T x) // optional set of fictional parameter(s)
3{
4    // simple requirement: expression must be valid
5    x++;    // expression must be valid
6    
7    // type requirement: `typename T`, T type must be a valid type
8    typename T::value_type;
9    typename S<T>;
10
11    // compound requirement: {expression}[noexcept][-> Concept];
12    // {expression} -> Concept<A1, A2, ...> is equivalent to
13    // requires Concept<decltype((expression)), A1, A2, ...>
14    {*x};  // dereference must be valid
15    {*x} noexcept;  // dereference must be noexcept
16    // dereference must  return T::value_type
17    {*x} noexcept -> std::same_as<typename T::value_type>;
18    
19    // nested requirement: requires ConceptName<...>;
20    requires Addable<T>; // constraint Addable<T> must be satisfied
21};

کانسپت

Concept در واقع یک مجموعه‌ نام‌دار از چنین قیود یا ترکیب منطقی آن‌ها است. هم کانسپت و هم requires-expression به صورت یک مقدار بولی در زمان کامپایل رندر می‌شوند و می‌توانند مانند یک مقدار نرمال، برای نمونه به صورت if constexpr مورد استفاده قرار گیرند:

1template<typename T>
2concept Addable = requires(T a, T b)
3{
4    a + b;
5};
6
7template<typename T>
8concept Dividable = requires(T a, T b)
9{
10    a/b;
11};
12
13template<typename T>
14concept DivAddable = Addable<T> && Dividable<T>;
15
16template<typename T>
17void f(T x)
18{
19    if constexpr(Addable<T>){ /*...*/ }
20    else if constexpr(requires(T a, T b) { a + b; }){ /*...*/ }
21}

بند Requires

برای این که در عمل چیزی را مقید بکنیم باید از «بند الزام» (requires-clause) استفاده کنیم. این بند ممکن است درست پس از بلوک <>template ظاهر شود یا به عنوان آخرین عنصر اعلان تابع یا حتی به طور همزمان در هر دو جا دارای لامبدا باشد.

1template<typename T>
2requires Addable<T>
3auto f1(T a, T b) requires Subtractable<T>; // Addable<T> && Subtractable<T>
4
5auto l = []<typename T> requires Addable<T>
6    (T a, T b) requires Subtractable<T>{};
7
8template<typename T>
9requires Addable<T>
10class C;
11
12// infamous `requires requires`. First `requires` is requires-clause,
13// second one is requires-expression. Useful if you don't want to introduce new
14// concept.
15template<typename T>
16requires requires(T a, T b) {a + b;}
17auto f4(T x);

روش تمیزتر این است که از نام concept به جای کلیدواژه class/typename در لیست پارامتر تمپلیت استفاده کنیم:

1template<Addable T>
2void f();

پارامترهای تمپلیت نیز می‌توانند مقید شوند. در این حالت آرگومان باید حداکثر به اندازه پارامتر مقید شود. پارامترهای نامقید تمپلیت همچنان می‌توانند تمپلیت‌های مقید را به عنوان آرگومان بپذیرند:

1template<typename T>
2concept Integral = std::integral<T>;
3
4template<typename T>
5concept Integral4 = std::integral<T> && sizeof(T) == 4;
6
7// requires-clause also works here
8template<template<typename T1> requires Integral<T1> typename T>
9void f2(){}
10
11// f() and f2() forms are equal
12template<template<Integral T1> typename T>
13void f(){
14    f2<T>();
15}
16
17// unconstrained template template parameter can accept constrained arguments
18template<template<typename T1> typename T>
19void f3(){}
20
21template<typename T>
22struct S1{};
23
24template<Integral T>
25struct S2{};
26
27template<Integral4 T>
28struct S3{};
29
30void test(){
31    f<S1>();    // OK
32    f<S2>();    // OK
33    // error, S3 is constrained by Integral4 which is more constrained than
34    // f()'s Integral
35    f<S3>();
36
37    // all are OK
38    f3<S1>();
39    f3<S2>();
40    f3<S3>();
41}

تابع‌های دارای قیود برآورد نشده، ناپیدا می‌شوند:

1template<typename T>
2struct X{
3    void f() requires std::integral<T>
4    {}
5};
6
7void f(){
8    X<double> x;
9    x.f();  // error
10    auto pf = &X<double>::f;    // error
11}

پارامترهای auto مقید

در نسخه جدید زبان برنامه‌نویسی ++C پارامترهای auto برای تابع‌های نرمال مجاز شمرده شده‌اند تا آن‌ها را مانند لامبداهای ژنریک به صورت ژنریک دربیاورند. کانسپت‌ها نیز می‌توانند برای مقید ساختن انواع placeholder در چارچوب‌های مختلف استفاده شوند. در پک‌های پارامتری MyConcept... Ts الزام می‌کند که MyConcept برای هر عنصر پک به صورت تک‌ به تک true ارزیابی شود و نه برای کل پک به صورت یک‌جا. برای نمونه باید به صورت requires<T1> && requires<T2> && ... && requires<TLast> باشد.

1template<typename T>
2concept is_sortable = true;
3
4auto l = [](auto x){};
5void f1(auto x){}               // unconstrained template
6void f2(is_sortable auto x){}   // constrained template
7
8template<is_sortable auto NonTypeParameter, is_sortable TypeParameter>
9is_sortable auto f3(is_sortable auto x, auto y)
10{
11    // notice that nothing is allowed between constraint name and `auto`
12    is_sortable auto z = 0;
13    return 0;
14}
15
16template<is_sortable auto... NonTypePack, is_sortable... TypePack>
17void f4(TypePack... args){}
18
19int f();
20
21// takes two parameters
22template<typename T1, typename T2>
23concept C = true;
24// binds second parameter
25C<double> auto v = f(); // means C<int, double>
26
27struct X{
28    operator is_sortable auto() {
29        return 0;
30    }
31};
32
33auto f5() -> is_sortable decltype(auto){
34    f4<1,2,3>(1,2,3);
35    return new is_sortable auto(1);
36}

مرتب‌سازی جزئی به وسیله قیود

قیود علاوه بر تعیین الزامات برای یک اعلان منفرد می‌توانند برای انتخاب بهترین جایگزین برای یک تابع نرمال، تابع تمپلیت یا یک تمپلیت کلاس نیز استفاده شوند. به این منظور قید‌ها یک نماد مرتب‌سازی جزئی دارند. به این ترتیب یک قید می‌تواند «کمتر» یا «بیشتر» از قید دیگری محدود شود و یا این که نامرتب باشد. کامپایلر قید‌ها را به صورت یک عطف/فصل اتمیک از قیدها تجزیه می‌کند. به طور شهودی C1 && C2 مقیدتر از C1 است، C1 مقیدتر از C1 || C2 است و هر قید مقیدتر از اعلان نامقید است. هنگامی که یک نامزد با قید برآورده شده وجود داشته باشد، آن که مقیدتر است انتخاب می‌شود. اگر قیدها نامرتب باشند، کاربرد مبهم خواهد بود.

1template<typename T>
2concept integral_or_floating = std::integral<T> || std::floating_point<T>;
3
4template<typename T>
5concept integral_and_char = std::integral<T> && std::same_as<T, char>;
6
7void f(std::integral auto){}        // #1
8void f(integral_or_floating auto){} // #2
9void f(std::same_as<char> auto){}   // #3
10
11// calls #1 because std::integral is more constrained
12// than integral_or_floating(#2)
13f(int{});
14// calls #2 because it's the only one whose constraint is satisfied
15f(double{});
16// error, #1, #2 and #3's constraints are satisfied but unordered
17// because std::same_as<char> appears only in #3
18f(char{});
19
20void f(integral_and_char auto){}    // #4
21
22// calls #4 because integral_and_char is more
23// constrained than std::same_as<char>(#3) and std::integral(#1)
24f(char{});

درک شیوه تجزیه قیدها از سوی کامپایلر حائز اهمیت است. زمانی که کامپایلر ببیند قیدهای اتمیک مشترک دارد، بین آن‌ها استنتاج می‌کند. در طی زمان تجزیه، نام کانسپت با تعریف آن جایگزین می‌شود، اما requires-expression دیگر بیش از این تجزیه نخواهد شد. دو قید اتمیک تنها زمانی یکسان هستند که با عبارت واحدی در مکان یکسان بازنمایی شده باشند. برای نمونه concept C = C1 && C2 به عطف C1 و C2 تجزیه می‌شود، اما concept C = requires{...} به concept C = Expression-Location-Pair تبدیل می‌شود و بدنه آن دیگر تجزیه نمی‌شود. اگر دو کانسپت الزامات مشترک یا حتی یکسانی در requires-expression خود داشته باشند، همواره نامرتب خواهند بود، زیرا یا مقادیر requires-expression آن‌ها برابر نیست و یا اگر هم برابر باشد، در مکان‌های مبدأ متفاوتی قرار دارد. همین اتفاق در زمان کاربرد تکراری خصیصه‌های از نوع عریان رخ می‌دهد، زیرا این موارد همواره قید‌های اتمیک متفاوتی را به جهت مکان‌های خود نمایش می‌دهند و از این رو نمی‌توانند به منظور مرتب‌سازی مورد استفاده قرار گیرند.

1template<typename T>
2requires std::is_integral_v<T>  // uses type traits instead of concepts
3void f1(){}  // #1
4
5template<typename T>
6requires std::is_integral_v<T> || std::is_floating_point_v<T>
7void f1(){}  // #2
8
9// error, #1 and #2 have common `std::is_integral_v<T>` expression
10// but at different locations(line 2 vs. line 6), thus, #1 and #2 constraints
11// are unordered and the call is ambiguous
12f1(int{});
13
14template<typename T>
15concept C1 = requires{      // requires-expression is not decomposed
16    requires std::integral<T>;
17};
18
19template<typename T>
20concept C2 = requires{      // requires-expression is not decomposed
21    requires (std::integral<T> || std::floating_point<T>);
22};
23
24void f2(C1 auto){}  // #3
25void f2(C2 auto){}  // #4
26
27// error, since requires-expressions are not decomposed, #3 and #4 have
28// completely unrelated and hence unordered constraints and the call is
29// ambiguous
30f2(int{});

تابع‌هhی عضو خاص بدیهی شرطی

در مورد پوشش‌هایی (wrapper) مانند std::optional یا std::variant بهتر است میزان «بدیهی بودن» از روی انواعی که پوشش‌ می‌دهند مشخص شود. برای نمونه <std::optional<int باید «بدیهی» (trivial) باشد، اما <std::optional<std::string نباید چنین باشد. در C++17 این کار از طریق استفاده از pretty cumbersome machinery انجام می‌یافت. کانسپت‌ها یک راه‌حل طبیعی برای این منظور دارند. ما امکان ساخت چند نسخه از یک تابع عضویت واحد را با قیدهای مختلف داریم و کامپایلر بهترین تابع را انتخاب کرده و بقیه را نادیده می‌گیرد. در این مورد خاص، وقتی نوع پوشش یافته از نوع بدیهی باشد، به یک مجموعه بدیهی از تابع‌ها نیاز داریم و هنگامی که چنین نباشد به یک مجموعه نابدیهی نیاز داریم. در C++17 یک کلاس کپی‌پذیر از نظر بدیهی بودن یا باید همه عملیات‌های کپی و جابجایی‌اش حذف شود و یا بدیهی باشد. برای این که کانسپت‌ها را نیز در نظر بگیریم، مفهوم «تابع عضویت خاص مطلوب» (eligible special member function) معرفی شده است. این تابعی است که حذف نشده و قید‌های آن (در صورت وجود) برآورده شده‌اند و هیچ تابع عضویت دیگری از این نوع با همان نوع پارامترها (در صورت وجود) مقیدتر نیست. به بیان ساده‌تر، این تابعی است که دارای مقیدترین قید‌های برآورده شده است. همه تجزیه‌گرهای موجود در این حالت «تجزیه‌گرهای پیش‌بین» (prospective destructors) نامیده می‌شوند. تنها یک تجزیه‌گر فعال مجاز است وجود داشته باشد و از طریق تصمیم‌گیری معمولی overload انتخاب می‌شود.

اکنون یک کلاس کپی‌پذیر از نظر بدیهی بودن به کلاسی گفته می‌شود که یک تجزیه‌گر حذف نشده و دست کم یک عملیات کپی/جابجایی مشروع دارد و همه چنین عملیات‌های مشروع آن از نوع بدیهی هستند. یک کلاس بدیهی زمانی از نظر بدیهی بودن کپی‌پذیر نامیده می‌شود که یک یا چند سازه پیش‌فرض مشروع داشته باشد که همگی بدیهی باشند. اساس طرز کار این تکنیک چنین است:

1template<typename T>
2class optional{
3public:
4    optional() = default;
5
6    // trivial copy-constructor
7    optional(const optional&) = default;
8
9    // non-trivial copy-constructor
10    optional(const optional& rhs)
11        requires(!std::is_trivially_copy_constructible_v<T>){
12        // ...
13    }
14
15    // trivial destructor
16    ~optional() = default;
17
18    // non-trivial destructor
19    ~optional() requires(!std::is_trivial_v<T>){
20        // ...
21    }
22    // ...
23private:
24    T value;
25};
26
27static_assert(std::is_trivial_v<optional<int>>);
28static_assert(!std::is_trivial_v<optional<std::string>>);

ماژول‌ها

ماژول‌ها یک روش جدید برای سازماندهی کد ++C در کامپوننت‌های منطقی هستند. از نظر تاریخی ++C از مدل C استفاده می‌کند که بر مبنای پیش‌پردازشگر و شمول متنی مکرر است. این روش مشکلات زیادی از قبیل نشت ماکروها به سمت داخل و بیرون هدرها، هدرهای با وابستگی به ترتیب شمول، کامپایل شدن مکرر کد یکسان، وابستگی‌های دوری، کپسوله‌سازی ضعیف جزییات پیاده‌سازی و بسیاری موارد دیگر دارد. ماژول‌ها برای حل این مشکلات معرفی شده‌اند ولی شاید این کار را با سرعت زیادی انجام ندهند. انتظار نمی‌رود تا زمانی که کامپایلرها و ابزارهای بیلد مانند CMake از ماژول‌ها پشتیبانی نکرده‌اند، بتوانیم از همه توان آن‌‌ها بهره بگیریم. توضیح کامل ماژول‌ها فراتر از حیطه این مقاله است، ما در اینجا صرفاً برخی ایده‌های مقدماتی را نشان می‌دهیم.

ایده اصلی خلق ماژول‌ها، محدودسازی موارد دسترس‌پذیر (اکسپورت شده) در زمان استفاده (ایمپورت کردن) کلاینت‌ها از یک ماژول بوده است. به این ترتیب می‌توانیم جزییات پیاده‌سازی را به طور واقعی پنهان کنیم.

1// module.cpp
2// dots in module name are for readability purpose, they have no special meaning
3export module my.tool;  // module declaration
4
5export void f(){}       // export f()
6void g(){}              // but not g()
7
8// client.cpp
9import my.tool;
10
11f();    // OK
12g();    // error, not exported

ماژول‌ها چندان با ماکروها سازگار نیستند. شما نمی‌توانید ماکروهایی که به طور دستی تعریف شده‌اند را به ماژول‌ها ارسال کنید و تنها در یک مورد خاص می‌توانید ماکروها را از ماژول ایمپورت کنید. ماژول‌ها نمی‌توانند وابستگی‌های دوری داشته باشند. ماژول یک موجودیت خود-گویا است. کامپایلر می‌تواند هر ماژول را دقیقاً یک بار پیش-کامپایل کند و از این رو زمان کلی کامپایل به میزان زیادی بهبود می‌یابد. ترتیب ایمپورت برای ماژول‌ها اهمیتی ندارد.

واحدهای ماژول

ماژول می‌تواند یک واحد ماژول اینترفیس یا واحد ماژول پیاده‌سازی باشد. تنها واحدهای اینترفیس می‌توانند در اینترفیس ماژول مشارکت داشته باشند. به همین دلیل است که در اعلان خود export می‌شوند. ماژول می‌تواند یک فایل منفرد یا روی پارتیشن‌های مختلف پراکنده باشد. هر پارتیشن به شکل module_name:partition_name نام‌گذاری می‌شود. این پارتیشن‌ها تنها درون همان ماژول قابل ایمپورت هستند و کلاینت تنها می‌تواند یک ماژول را ب طور کامل ایمپورت کند. این روش نسبت به فایل‌های هدر کسپوله‌سازی بهتری ارائه می‌کند.

1// tool.cpp
2export module tool; // primary module interface unit
3export import :helpers; // re-export(see below) helpers partition
4
5export void f();
6export void g();
7
8// tool.internals.cpp
9module tool:internals;  // implementation partition
10void utility();
11
12// tool.impl.cpp
13module tool;    // implementation unit, implicitly imports primary module unit
14import :internals;
15
16void utility(){}
17
18void f(){
19    utility();
20}
21
22// tool.impl2.cpp
23module tool;    // another implementation unit
24void g(){}
25
26// tool.helpers.cpp
27export module tool:helpers; // module interface partition
28import :internals;
29
30export void h(){
31    utility();
32}
33
34// client.cpp
35import tool;
36
37f();
38g();
39h();

توجه کنید که پارتیشن‌ها بدون ذکر نام ماژول ایمپورت شده‌اند. به این ترتیب ایمپورت کردن پارتیشن‌های ماژول‌های دیگر ممنوع می‌شود. چندین واجد پیاده‌سازی مجاز هستند و همه واحدهای دیگر و پارتیشن‌ها از هر نوع باید یکتا باشند. همه پارتیشن‌های اینترفیس باید به وسیله ماژول و از طریق export import دوباره اکسپورت شوند.

اکسپورت

در این بخش شکل‌های مختلف export را بررسی می‌کنیم. قاعده کلی این است که نمی‌توانید نام‌های دارای پیوند درونی را اکسپورت کنید:

1// tool.cpp
2module tool;
3export import :helpers; // import and re-export helpers interface partition
4
5export int x{}; // export single declaration
6
7export{         // export multiple declarations
8    int y{};
9    void f(){};
10}
11
12export namespace A{ // export the whole namespace
13    void f();
14    void g();
15}
16
17namespace B{
18    export void f();// export a single declaration within a namespace
19    void g();
20}
21
22namespace{
23    export int x;   // error, x has internal linkage
24    export void f();// error, f() has internal linkage
25}
26
27export class C; // export as incomplete type
28class C{};
29export C get_c();
30
31// client.cpp
32import tool;
33
34C c1;    // error, C is incomplete
35auto c2 = get_c();  // OK

ایمپورت

اعلان‌های ایمپورت باید مقدم بر هر نوع اعلان‌های غیر ماژولی باشند چون باعث تحلیل سریع‌تر وابستگی می‌شود. علاوه بر این که این روش کاملاً شهودی و سرراست است.

1// tool.cpp
2export module tool;
3import :helpers;  // import helpers partition
4
5export void f(){}
6
7// tool.helpers.cpp
8export module tool:helpers;
9
10export void g(){}
11
12// client.cpp
13import tool;
14
15f();
16g();

واحدهای هدر

یک import خاص وجود دارد که امکان ایمپورت کردن هدرهای قابل ایمپورت یعنی <import <header.h یا "import "header.h را فراهم می‌سازد. کامپایلر یک واحد هدر سنتز شده ایجاد کرده و همه اعلان‌ها را به طور ضمنی اکسپورت می‌کند. این که در عمل کدام هدرها قابل ایمپورت هستند در مرحله پیاده‌سازی تعریف می‌شود، اما همه هدرهای کتابخانه ++C چنین هستند. شاید روشی باشد که بتوان به کامپایلر اعلام کرد کدام هدرهای ارائه شده از سوی کاربر قابل ایمپورت هستند. چنین هدرهایی نباید شامل تابع‌های غیر «درون خطی» (inline) یا متغیرهایی با پیوند به بیرون باشند. این تنها شکلی از import است که امکان ایمپورت کردن ماکروها را از هدرها فراهم می‌سازد. با این حال همچنان نمی‌توانید آن‌ها را از طریق "export import "header.h مجدداً اکسپورت کنید. اگر در مورد محتوای هدر مطمئن نیستید، از این روش برای ایمپورت کردن هدرهای قدیمی به طور شانسی استفاده نکنید.

قطعه ماژول سراسری

اگر نیاز به استفاده از هدرهای سبک قدیم درون یک ماژول باشد، یک مکان مطمئن برای قرار دادن امن include-ها وجود دارد و آن «قطعه ماژول سراسری» (Global module fragment) است.

1// header.h
2#pragma once
3class A{};
4void g(){}
5
6// tool.cpp
7module;             // global module fragment
8#include "header.h"
9export module tool; // ends here
10
11export void f(){    // uses declarations from header.h
12    g();
13    A a;
14}

این دستور باید قبل از اعلان ماژول نام‌دار ظاهر شود و صرفاً می‌تواند شامل دایرکتیو‌های پیش‌پردازشگر باشد. همه اعلان‌هایی که در قطعه ماژول سراسری قرار دارند و همچنین واحدهای بازگردانی غیر ماژولار به یک ماژول سراسری منفرد متصل می‌شوند. از این رو همه قواعد مربوط به هدرهای نرمال در این مورد نیز صادق است.

قطعه ماژول خصوصی

آخرین بخش عجیب ماجرا «قطعه ماژول خصوصی» (Private module fragment) است. هدف از این قطعه آن است که جزئیات پیاده‌سازی در یک ماژول تک‌فایلی پنهان شود. از لحاظ نظری، کلاینت‌ها ممکن است نتوانند در صورت تغییر یافتن موارد مختلف درون فرگمان ماژول خصوصی آن را مجدداً کامپایل کنند.

1export module tool; // interface
2
3export void f();    // declared here
4
5module :private;    // implementation details
6
7void f(){}          // defined here

حذف inline ضمنی

یکی دیگر از تغییرات جالب در C++20 با inline مرتبط است. تابع‌های عضویت که درون بخش تعاریف کلاس تعریف می‌شوند، در صورت الصاق کلاس به یک ماژول نام‌دار دیگر نمی‌توانند به طور ضمنی inline شوند. تابع‌های inline در یک ماژول نام‌دار تنها می‌توانند از نام‌هایی استفاده کنند که در معرض دید کلاینت باشند.

1// header.h
2struct C{
3    void f(){}  // still inline because attached to a global module
4};
5
6// tool.cpp
7module;
8#include "header.h"
9
10export module tool;
11
12class A{};  // not exported
13
14export struct B{// B is attached to module "tool"
15    void f(){   // not implicitly inline anymore
16        A a;    // can safely use non-exported name
17    }
18
19    inline void g(){
20        A a;    // oops, uses non-exported name
21    }
22
23    inline void h(){
24        f();    // fine, f() is not inline
25    }
26};
27
28// client.cpp
29import tool;
30
31B b;
32b.f();  // OK
33b.g();  // error, A is undefined
34b.h();  // OK

کوروتین‌ها

در نهایت ما شاهد معرفی کوروتین‌های غیر پشته‌ای (چون در هیپ ذخیره می‌شوند) در C++ 20 هستیم. در این نسخه از زبان برنامه‌نویسی سی پلاس پلاس تقریباً پایین‌ترین سطح از API عرضه شده است و باقی کار بر عهده کاربر قرار گرفته است. کلیدواژه‌های co_await, co_yield و co_return و قواعد برای تعامل بین فراخواننده و فراخوانده شده هستند. این قواعد چنان سطح پایین هستند که توضیحان ها در این جا ضرورتی ندارد. خوشبختانه نسخه C++23 این شکاف را تا حدودی پر کرده است. تا آن زمان می‌توان از کتابخانه‌های شخص ثالث استفاده کرد. برای نمونه در مثال زیر از کتابخانه cppcoro استفاده کرده‌ایم:

1cppcoro::task<int> someAsyncTask()
2{
3    int result;
4    // get the result somehow
5    co_return result;
6}
7
8// task<> is analog of void for normal function
9cppcoro::task<> usageExample()
10{
11    // creates a new task but doesn't start executing the coroutine yet
12    cppcoro::task<int> myTask = someAsyncTask();
13    // ...
14    // Coroutine is only started when we later co_await the task.
15    auto result = co_await myTask;
16}
17
18// will lazily generate numbers from 0 to 9
19cppcoro::generator<std::size_t> getTenNumbers()
20{
21    std::size_t n{0};
22    while (n != 10)
23    {
24        co_yield n++;
25    }
26}
27
28void printNumbers()
29{
30    for(const auto n : getTenNumbers())
31    {
32        std::cout << n;    
33    }
34}

مقایسه سه‌طرفه

تا پیش از C++20 برای عرضه عملیات مقایسه در یک کلاس، نیاز به پیاده‌سازی شش عملگر به صورت ==,!=, <, <=, >, >= بود. به طور معمول، چهار مورد از این عملگرها شامل کد قالبی هستند که بر حسب == و < عمل می‌کنند که شامل منطق مقایسه عملی هستند. رویه معمول این است که آن‌ها را به صورت تابع‌های آزادی پیاده‌سازی کنیم که const T& را می‌گیرند تا بتوانند انواع تبدیل‌پذیر را مقایسه کنند. اگر بخواهید از انواع غیر تبدیل‌پذیر نیز پشتیبانی کنید، باید دو مجموعه از تابع یعنی op(const T1&, const T2&) و op(const T2&, const T1&) را اضافه کنید که موجب می‌شود 18 عملگر مقایسه داشته باشیم. C++20 یک روش بهتر برای مدیریت و درک مقایسه‌ها به ما می‌دهد. اکنون می‌توانید روی operator<=>() و گاهی اوقات روی ()==operator متمرکز شوید. اینک <=>operator جدید که به نام عملگر فضاپیما مشهور است، مقایسه سه طرفه را اجرا می‌کند. این عملگر با یک فراخوانی منفرد اعلام می‌کند که آیا a کوچک‌تر، مساوی یا بزرگتر از b است و روش کار آن شبیه ()strcmp است. این عملگر دسته‌بندی مقایسه (در ادامه توضیح داده شده) بازگشت می‌دهد که می‌تواند با صفر مقایسه شود. کامپایلر با داشتن این امکان می‌تواند فراخوانی‌‌های به <, <=, >, >= را با فراخوانی‌ها به ()<=>operator جایگزین و نتیجه آن را بررسی کند. به این ترتیب a < b به صورت a <=> b < 0 درمی‌آید. همچنین فراخوانی‌ها به ==,!= با عملگر ==() عوض می‌شود، یعنی a!= b به صورت (a == b)! درمی‌آید. با توجه به قواعد جدید وارسی، امکان مدیریت مقایسه‌های نامتقارن نیز وجود دارد. به این ترتیب با ارائه یک مقایسه T1::operator==(const T2&) به صورت منفرد، هر دو مقایسه T1 == T2 و T2 == T1 به دست می‌آید و همین نکته در مورد ()<=>operator نیز صدق می‌کند. اکنون باید حداکثر دو تابع بنویسید تا شش مقایسه بین انواع تبدیل‌پذیر انجام یابد. همچنین با نوشتن 2 تابع، 12 مقایسه بین انواع تبدیل‌ناپذیر صورت می‌گیرد.

دسته‌بندی مقایسه‌ها

به طور استاندارد سه دسته مقایسه وجود دارد، اما شما برای ایجاد دسته‌بندی‌های سفارشی منعی ندارید. strong_ordering به این معنی است که دقیقاً یکی از مقایسه‌های a < b, a > b و a == b باید true باشد و اگر a == b، در این صورت f(a) == f(b) است. weak_ordering به این معنی است که دقیقاً یکی از a < b, a > b, a == b باید ‌true باشد و اگر a == b در این صورت f(a) می‌تواند برابر با f(b) نباشد. چنین عناصر معادل هم هستند، اما برابر نیستند. partial_ordering به این معنی است که ممکن است هیچ کدام از a < b, a > b, a == b مقدار TRUE نداشته باشند و اگر == b در این صورت f(a) نمی‌تواند برابر با f(b) باشد. معنی این حرف آن است که برخی عناصر ممک است مقایسه‌ناپذیر باشند. نکته مهم در اینجا آن است که f() نمایانگر تابعی است که تنها به خصوصیت‌های برجسته دسترسی دارد. برای نمونه std::vector<int> به طور کامل مرتب شده به جز این که دو بردار با مقدار یکسان می‌توانند ظرفیت متفاوتی داشته باشند. در اینجا ظرفیت یک خصوصیت برجسته نیست. نمونه یک نوع با مرتب‌سازی ضعیف CaseInsensitiveString است که می‌تواند رشته را همان طور که هست ذخیره کند و در عین حال به روش حساس به بزرگی/کوچکی حروف مقایسه کند. مثالی از نوع با مرتب‌سازی جزئی float/double است زیرا NaN با دیگر نوع‌ها قابل مقایسه نیست. این دسته‌بندی‌های نوعی سلسله مرhتf تشکیل می‌دهند، یعنی مرتب‌سازی قوی می‌تواند به مرتب‌سازی جزئی و مرتب‌سازی جزئی نیز می‌تواند به مرتب‌سازی ضعیف تبدیل شود.

مقایسه‌های پیش‌فرضی

برای مقایسه‌ها نیز می‌توان مانند تابع‌های عضویت خاص، مقادیر پیش‌فرض تعریف کرد. در چنین مواردی مقایسه‌ها به روش عضویت-گونه عمل کرده و همه اعضای داده‌ای غیر استاتیک زیرین را با عملگرهای متناظرشان مقایسه می‌کنند. ()<=>operator پیش‌فرضی یک عملگر ()==operator پیش‌فرضی نیز اعلان می‌کند و از این رو می‌توان دستوری مانند ;auto operator<=>(const T&) const = default نوشت و همه شش عملگر مقایسه‌ای را با معناشناسی عضویت‌گونه به دست آورد.

1template<typename T1, typename T2>
2void TestComparisons(T1 a, T2 b)
3{
4    (a < b), (a <= b), (a > b), (a >= b), (a == b), (a != b);
5}
6
7struct S2
8{
9    int a;
10    int b;
11};
12
13struct S1
14{
15    int x;
16    int y;
17    // support homogeneous comparisons
18    auto operator<=>(const S1&) const = default;
19    // this is required because there's operator==(const S2&) which prevents
20    // implicit declaration of defaulted operator==()
21    bool operator==(const S1&) const = default;
22
23    // support heterogeneous comparisons
24    std::strong_ordering operator<=>(const S2& other) const
25    {
26        if (auto cmp = x <=> other.a; cmp != 0)
27            return cmp;
28        return y <=> other.b;
29    }
30
31    bool operator==(const S2& other) const
32    {
33        return (*this <=> other) == 0;
34    }
35};
36
37TestComparisons(S1{}, S1{});
38TestComparisons(S1{}, S2{});
39TestComparisons(S2{}, S1{});

در کد فوق ()==operator که به طور ضمنی اعلان شده همان امضای ()<=>operator را دارد، به جز این که نوع بازگشتی bool است.

1template<typename T>
2struct X
3{
4    friend constexpr std::partial_ordering operator<=>(X, X) requires(sizeof(T) != 1) = default;
5    // implicitly declares:
6    // friend constexpr bool operator==(X, X) requires(sizeof(T) != 1) = default;
7
8    [[nodiscard]] virtual std::strong_ordering operator<=>(const X&) const = default;
9    // implicitly declares:
10    //[[nodiscard]] virtual bool operator==(const X&) const = default; 
11};

دسته‌بندی مقایسه استنتاج شده، ضعیف‌ترین دسته‌بندی از اعضای نوع است.

1struct S3{
2    int x;      // int-s are strongly ordered
3    double d;   // but double-s are partially ordered
4    // thus, the resulting category is std::partial_ordering
5    auto operator<=>(const S3&) const = default;
6};
7static_assert(std::is_same_v<decltype(S3{} <=> S3{}), std::partial_ordering>);

آن‌ها باید عضو یا دوست باشند و تنها دوستان می‌توانند به صورت با مقدار دریافت شوند.

1struct S4
2{
3    int x;
4    int y;
5    // member version must have op(const T&) const; form
6    auto operator<=>(const S3&) const = default;
7
8    // friend version can take arguments by const-reference or by-value
9    // friend auto operator<=>(const S3&, const S3&) = default;
10    // friend auto operator<=>(S3, S3) = default;
11};

این موارد می‌توانند درست مانند تابع‌های عضویت خاص دارای مقادیر پیش‌فرض خارج از کلاس باشند.

1struct S5
2{
3    int x;
4    std::strong_ordering operator<=>(const S5&) const;
5    bool operator==(const S5&) const;
6};
7
8std::strong_ordering S5::operator<=>(const S5&) const = default;
9bool S5::operator==(const S5&) const = default;

عملگر پیش‌فرضی ()<=>operator که از ()<=>operator از اعضای کلاس یا ترتیب‌بندی آن‌ها استفاده می‌کند، می‌تواند با استفاده از ()==Member::operator و ()<Member::operator سنتز شود. توجه کنید که این مقدار تنها برای اعضا کار می‌کند و نه خود کلاس. از این رو ()<T::operator که موجود است هرگز در ()<=>T::operator استفاده نمی‌شود.

1// not in our immediate control
2struct Legacy
3{
4    bool operator==(Legacy const&) const;
5    bool operator<(Legacy const&) const;
6};
7
8struct S6
9{
10    int x;
11    Legacy l;
12    // deleted because Legacy doesn't have operator<=>(), comparison category
13    // can't be deduced
14    auto operator<=>(const S6&) const = default;
15};
16
17struct S7
18{
19    int x;
20    Legacy l;
21
22    std::strong_ordering operator<=>(const S7& rhs) const = default;
23    /*
24    Since comparison category is provided explicitly, ordering can be
25    synthesized using operator<() and operator==(). They must return exactly
26    `bool` for this to work. It will work for weak and partial ordering as well.
27    
28    Here's an example of synthesized operator<=>():
29    std::strong_ordering operator<=>(const S7& rhs) const
30    {
31        // use operator<=>() for int
32        if(auto cmp = x <=> rhs.x; cmp != 0) return cmp;
33
34        // synthesize ordering for Legacy using operator<() and operator==()
35        if(l == rhs.l) return std::strong_ordering::equal;
36        if(l < rhs.l) return std::strong_ordering::less;
37        return std::strong_ordering::greater;
38    }
39    */
40};
41
42struct NoEqual
43{
44    bool operator<(const NoEqual&) const = default;
45};
46
47struct S8
48{
49    NoEqual n;
50    // deleted, NoEqual doesn't have operator<=>()
51    // auto operator<=>(const S8&) const = default;
52
53    // deleted as well because NoEqual doesn't have operator==()
54    std::strong_ordering operator<=>(const S8&) const = default;
55};
56
57struct W
58{
59    std::weak_ordering operator<=>(const W&) const = default;
60};
61
62struct S9
63{
64    W w;
65    // ask for strong_ordering but W can provide only weak_ordering, this will
66    // yield an error during instantiation
67    std::strong_ordering operator<=>(const S9&) const = default;
68    void f()
69    {
70        (S9{} <=> S9{});    // error
71    }
72};

Union و اعضای رفرنس از این خصوصیت پشتیبانی نمی‌کنند.

1struct S4
2{
3    int& r;
4    // deleted because of reference member
5    auto operator<=>(const S4&) const = default;
6};

عبارت‌های لامبدا

در این بخش با نقش عبارت‌های لامبدا در C++20 آشنا می‌شویم.

عبارت this زمانی که به صورت ضمنی دریافت شود همواره به صورت «با ارجاع» دریافت می‌شود هر چند از [=] استفاده شده باشد. برای رفع این سردرگمی، C++20 این رفتار را منسوخ کرده و امکان استفاده از [=, this] را فراهم ساخته که صراحت بیشتری دارد.

struct S

1struct S{
2    void f(){
3        [=]{};          // captures this by reference, deprecated since C++20
4        [=, *this]{};   // OK since C++17, captures this by value
5        [=, this]{};    // OK since C++20, captures this by reference
6    }
7};

لیست پارامتر تمپلیت برای لامبداهای ژنریک

گاهی اوقات لامبداهای ژنریک بیش از حد ژنریک می‌شوند. C++20 امکان استفاده از ساختار آشناتر تمپلیت را برای معرفی مستقیم نام‌های نوع فراهم ساخته است.

1// lambda that expect std::vector<T>
2// until C++20:
3[](auto vector){
4    using T =typename decltype(vector)::value_type;
5    // use T
6};
7// since C++20:
8[]<typename T>(std::vector<T> vector){
9    // use T
10};
11
12// access argument type
13// until C++20
14[](const auto& x){
15    using T = std::decay_t<decltype(x)>;
16    // using T = decltype(x); // without decay_t<> it would be const T&, so
17    T copy = x;               // copy would be a reference type
18    T::static_function();     // and these wouldn't work at all
19    using Iterator = typename T::iterator;
20};
21// since C++20
22[]<typename T>(const T& x){
23    T copy = x;
24    T::static_function();
25    using Iterator = typename T::iterator;
26};
27
28// perfect forwarding
29// until C++20:
30[](auto&&... args){
31    return f(std::forward<decltype(args)>(args)...);
32};
33// since C++20:
34[]<typename... Ts>(Ts&&... args){
35    return f(std::forward<Ts>(args)...);
36};
37
38// and of course you can mix them with auto-parameters
39[]<typename T>(const T& a, auto b){};

لامبداها در ساختارهای غیر ارزیابی شده

عبارت‌های لامبدا می‌توانند در ساختارهای غیر ارزیابی شده مانند ()sizeof(), typeid(), decltype و غیره نیز استفاده شوند. در این بخش برخی نکات کلیدی این قابلیت را برای یک مثال عملیاتی مشاهده می‌کنیم. مفهوم اصلی این است که لامبداها یک نوع ناشناس منحصربه‌فرد دارند، به طوری که دو لامبدا و نوع‌های آن‌ها هرگز با هم برابر نمی‌شوند.

1using L = decltype([]{});   // lambdas have no linkage
2L PublicApi();              // L can't be used for external linkage
3
4// in template , two different declarations
5template<class T> void f(decltype([]{}) (*s)[sizeof(T)]);
6template<class T> void f(decltype([]{}) (*s)[sizeof(T)]);
7
8// again, lambda types are never equivalent
9static decltype([]{}) f();
10static decltype([]{}) f(); // error, return type mismatch
11
12static decltype([]{}) g();
13static decltype(g()) g(); // okay, redeclaration
14
15// each specialization has its own lambda with unique type
16template<typename T>
17using R = decltype([]{});
18
19static_assert(!std::is_same_v<R<int>, R<char>>);
20
21// Lambda-based SFINAE and constraints are not supported, it just fails
22template <class T>
23auto f(T) -> decltype([]() { T::invalid; } ());
24void f(...);
25
26template<typename T>
27void g(T) requires requires{
28    [](){typename T::invalid x;}; }
29{}
30void g(...){}
31
32f(0);  // error
33g(0);  // error

در مثال زیر ()f یک شمارنده منفرد را در دو واحد ترجمه افزایش می‌دهد، زیرا تابع inline طوری عمل می‌کند که گویی تنها یک تعریف از آن وجود دارد. با این حال g_s از ODR تخطی می‌کند، زیرا گرچه یک تعریف از آن وجود دارد، اما همچنان چندین اعلان وجود دارند که متفاوت هستند، زیرا دو لامبدای مختلف در a.cpp و b.cpp وجود دارند و از این جهت S آرگومان تمپلیت غیر نوعی متفاوتی دارد:

1// a.h
2template<typename T>
3int counter(){
4    static int value{};
5    return value++;
6}
7
8inline int f(){
9    return counter<decltype([]{})>();
10}
11
12template<auto> struct S{ void call(){} };
13// cast lambda to pointer
14inline S<+[]{}> g_s;
15
16// a.cpp
17#include "a.h"
18auto v = f();
19g_s.call();
20
21// b.cpp
22#include "a.h"
23auto v = f();
24g_s.call();

لامبداهای پیش‌فرض بی‌حالت ساخت‌پذیر و انتساب‌پذیر

لامبداهای بی‌حالت در C++20 در واقع لامبداهای ساخت‌پذیر و انتساب‌پذیر پیش‌فرض هستند که امکان استفاده از یک نوع لامبدا را برای ساخت/انتساب در آینده فراهم می‌سازند. ما با استفاده از لامبداها در ساختارهای غیر ارزیابی‌شده می‌توان نوعی لامبدا به دست آوریم که دارای decltype() باشد و در ادامه متغیری با این نوع ایجاد کنیم:

1auto greater = [](auto x,auto y)
2{
3    return x > y;
4};
5// requires default constructible type
6std::map<std::string, int, decltype(greater)> map;
7auto map2 = map;    // requires default assignable type

در این کد std::map یک نوع مقایسه‌گر را دریافت می‌کند تا در ادامه وهله‌سازی کند. با این که در C++17 نیز می‌توانستیم نوع لامبدا داشته باشیم، اما امکان ساختن وهله از این نوع وجود داشت زیرا لامبداها به طور پیش‌فرض ساخت‌پذیر نبودند.

بسط پک در لامبدای init-capture

C++20 امکان دریافت پک‌های پارامتری را در لامبداها تسهیل کرده است. تا ‌C++20 این مقادیر را می‌توانستیم به صورت «با مقدار» و یا «با ارجاع» و یا با به کار بستن برخی ترفندها را صورت نیاز به جابجایی بسته با std::tuple دریافت کرد. اکنون کار بسیار آسان‌تر شده است و می‌توانیم پک init-capture را ایجاد کرده و آن را با پکی که می‌خواهیم دریافت کنیم وهله‌سازی کنیم. این وهله‌سازی محدود به std::move یا std::forward نیست و هر تابعی می‌تواند روی عناصر پک اعمال شود.

1void g(int, int){}
2
3// C++17
4template<class F, class... Args>
5auto delay_apply(F&& f, Args&&... args) {
6    return [f=std::forward<F>(f), tup=std::make_tuple(std::forward<Args>(args)...)]()
7            -> decltype(auto) {
8        return std::apply(f, tup);
9    };
10}
11
12// C++20
13template<typename F, typename... Args>
14auto delay_call(F&& f, Args&&... args) {
15    return [f = std::forward<F>(f), ...f_args=std::forward<Args>(args)]()
16            -> decltype(auto) {
17        return f(f_args...);
18    };
19}
20
21void f(){
22    delay_call(g, 1, 2)();
23}

عبارت‌های ثابت

تابع‌های بی‌درنگ (consteval)

با این که constexpr به طور ضمنی اشاره می‌کند که تابع می‌تواند در زمان کامپایل ارزیابی شود، اما consteval مشخص می‌کند که تابع باید صرفاً در زمان کامپایل ارزیابی شود. تابع‌های virtual مجاز هستند که به صورت consteval باشند، اما می‌توانند override شوند و یا صرفاً از سوی تابع consteval دیگری باطل شوند، یعنی ساخت ترکیبی از consteval و غیر consteval مجاز نیست. تابع‌های تخریب‌گر و تخصیص/آزادسازی نمی‌توانند consteval باشند.

1consteval int GetInt(int x){
2    return x;
3}
4
5constexpr void f(){
6    auto x1 = GetInt(1);
7    constexpr auto x2 = GetInt(x1); // error x1 is not a constant-expression
8}

تابع مجازی constexpr

تابع‌های مجازی اکنون می‌توانند constexpr باشند. تابع constexpr می‌تواند به صورت غیر constexpr و یا به طور برعکس باطل شود.

1struct Base{
2    constexpr virtual ~Base() = default;
3    virtual int Get() const = 0;    // non-constexpr
4};
5
6struct Derived1 : Base{
7    constexpr int Get() const override {
8        return 1;
9    }
10};
11
12struct Derived2 : Base{
13    constexpr int Get() const override {
14        return 2;
15    }
16};
17
18constexpr auto GetSum(){
19    const Derived1 d1;
20    const Derived2 d2;
21    const Base* pb1 = &d1;
22    const Base* pb2 = &d2;
23
24    return pb1->Get() + pb2->Get();
25}
26
27static_assert(GetSum() == 1 + 2);   // evaluated at compile-time

بلوک‌های try-catch در constexpr

قرارگیری بلوک‌های try-catch در نسخه جدید زبان برنامه‌نویسی سی پلاس پلاس درون تابع‌های constexpr مجاز است، اما throw چنین نیست و از این رو بلوک catch در عمل نادیده گرفته می‌شود. این وضعیت برای مثال می‌تواند در ترکیب با constexpr new مفید باشد چون می‌توانیم تابع منفردی داشته باشیم که در زمان اجرا/کامپایل کار می‌کند:

1constexpr void f(){
2    try{
3        auto p = new int;
4        // ...
5        delete p;
6    }
7    catch(...){     // ignored at compile-time
8        // ...
9    }
10}

constexpr dynamic_cast و typeid چندریختی

از آنجا که تابع‌های مجازی اکنون به صورت constexpr هستند، دلیلی وجود ندارد که dynamic_cast و typeid چندریختی درون constexpr مجاز باشد متأسفانه std::type_info هنوز هیچ عضو constexpr ندارد و از این رو فعلاً کاربرد کمی دارد.

1struct Base1{
2    virtual ~Base1() = default;
3    constexpr virtual int get() const = 0;
4};
5
6struct Derived1 : Base1{
7    constexpr int get() const override {
8        return 1;
9    }
10};
11
12struct Base2{
13    virtual ~Base2() = default;
14    constexpr virtual int get() const = 0;
15};
16
17struct Derived2 : Base2{
18    constexpr int get() const override {
19        return 2;
20    }
21};
22
23template<typename Base, typename Derived>
24constexpr auto downcasted_get(){
25    const Derived d;
26    const Base& upcasted = d;
27    const auto& downcasted = dynamic_cast<const Derived&>(upcasted);
28
29    return downcasted.get();
30}
31
32static_assert(downcasted_get<Base1, Derived1>() == 1);
33static_assert(downcasted_get<Base2, Derived2>() == 2);
34
35// compile-time error, cannot cast Derived1 to Base2
36static_assert(downcasted_get<Base2, Derived1>() == 1);

تغییر دادن عضو فعال یک union درون constexpr

یکی دیگر از بهبودهایی که در خصوص عبارت‌های ثابت رخ داده است، فراهم آمدن امکان تغییر دادن عضو فعال یک Union است، اما هنوز امکان خواندن یک عضو غیر فعال وجود ندارد، زیرا یک UB است و UB درون سازه constexpr مجاز نیست.

1union Foo {
2  int i;
3  float f;
4};
5
6constexpr int f() {
7  Foo foo{};
8  foo.i = 3;    // i is an active member
9  foo.f = 1.2f; // valid since C++20, f becomes an active member
10
11//   return foo.i;  // error, reading inactive union member
12  return foo.f;
13}

قابلیت‌های سی پلاس پلاس

تخصیص‌های constexpr

C++20 یک زیرساخت برای کانتینرهای constexpr فراهم ساخته است. در وهله نخست امکان استفاده از constexpr و حتی تخریب‌گر‌های virtual constexpr برای «انواع لفظی» (literal types) وجود دارد. در وهله دوم امکان فراخوانی به ()std::allocator<T>::allocate و new-expression وجود دارد که در صورت آزاد شدن فضای تخصیص یافته در زمان کامپایل، موجب یک فراخوانی به یکی از موارد سراسری operator new می‌شود. معنی این حرف آن است که حافظه می‌تواند در زمان کامپایل تخصیص یابد، اما باید در زمان کامپایل نیز آزاد شود. در صورتی که نیاز باشد، داده‌های نهایی در زمان اجرا مورد استفاده قرار گیرند، این وضعیت موجب بروز نوعی سردرگمی می‌شود. از این رو گریزی نیست جز ذخیره‌سازی در نوعی کانتینر غیر تخصیصی مانند std::array و دریافت دوگانه در زمان کامپایل: بار اول برای دریافت اندازه داده‌ها و بار دون برای دریافت یک کپی واقعی از آن.

1constexpr auto get_str()
2{
3    std::string s1{"hello "};
4    std::string s2{"world"};
5    std::string s3 = s1 + s2;
6    return s3;
7}
8
9constexpr auto get_array()
10{
11    constexpr auto N = get_str().size();
12    std::array<char, N> arr{};
13    std::copy_n(get_str().data(), N, std::begin(arr));
14    return arr;
15}
16
17static_assert(!get_str().empty());
18
19// error because it holds data allocated at compile-time
20constexpr auto str = get_str();
21
22// OK, string is stored in std::array<char>
23constexpr auto result = get_array();

مقداردهی پیش‌فرض آزمایشی در تابع‌های constexpr

در C++17 سازه constexpr علاوه بر دیگر الزامات باید همه اعضای داده‌ای غیر استاتیک خود را مقداردهی می‌کرد. این قاعده در C++20 حذف شده است، اما از آنجا که UB در سازه constexpr مجاز نیست، امکان خواندن از چنین اعضای مقداردهی نشده وجود ندارد و تنها می‌توان آن‌ها را نوشت.

1struct NonTrivial{
2    bool b = false;
3};
4
5struct Trivial{
6    bool b;
7};
8
9template <typename T>
10constexpr T f1(const T& other) {
11    T t;        // default initialization
12    t = other;
13    return t;
14}
15
16template <typename T>
17constexpr auto f2(const T& other) {
18    T t;
19    return t.b;
20}
21
22void test(){
23    constexpr auto a = f1(Trivial{});   // error in C++17, OK in C++20
24    constexpr auto b = f1(NonTrivial{});// OK
25
26    constexpr auto c = f2(Trivial{}); // error, uninitialized Trivial::b is used
27    constexpr auto d = f2(NonTrivial{}); // OK
28}

اعلان asm ارزیابی نشده در تابع‌های constexpr

اعلان asm اکنون می‌تواند در صورتی که در زمان کامپایل ارزیابی نشده باشد، درون تابع constexpr ظاهر شود. به این ترتیب می‌توان هم کد زمان اجرا و هم کد زمان کامپایل را درون یک تابع منفرد همزمان داشت:

1constexpr int add(int a, int b){
2    if (std::is_constant_evaluated()){
3        return a + b;
4    }
5    else{
6        asm("asm magic here");
7        //...
8    }
9}

تابع std::is_constant_evaluated()

اکنون با استفاده از تابع ()std::is_constant_evaluated می‌توانیم بررسی کنیم که آیا فراخوانی جاری درون یک سازه ارزیابی شده ثابت رخ داده است یا نه. البته ما وسوسه می‌شویم که از عبارت در زمان کامپایل استفاده کنیم، اما بر اساس مستندات سی پلاس پلاس هیچ تفاوت روشنی بین زمان اجرا و زمان کامپایل وجود ندارد. در عوض C++20 لیستی از عبارت‌ها ارائه کرده است که آن‌ها را «ارزیابی شده ثابت» در نظر می‌گیریم ین تابع در صورتی که در حال ارزیابی این موارد باشیم، مقدار true و در غیر این صورت مقدار False بازگشت می‌دهد.

باید مراقب بود که این تابع را مستقیماً درون چین عبارت‌های با ارزیابی ثابتی استفاده نکرد. بر اساس تعریف این تابع، اگر درون چنین عبارت‌هایی فراخوانی شود، حتی در صورتی که تابع محصور دارای ارزیابی ثابتی نباشد، مقدار true بازگشت خواهد داد.

1constexpr int GetNumber(){
2    if(std::is_constant_evaluated()){   // should not be `if constexpr`
3        return 1;
4    }
5    return 2;
6}
7
8constexpr int GetNumber(int x){
9    if(std::is_constant_evaluated()){   // should not be `if constexpr`
10        return x;
11    }
12    return x+1;
13}
14
15void f(){
16    constexpr auto v1 = GetNumber();
17    const auto v2 = GetNumber();
18
19    // initialization of a non-const variable, not constant-evaluated
20    auto v3 = GetNumber();
21
22    assert(v1 == 1);
23    assert(v2 == 1);
24    assert(v3 == 2);
25
26    constexpr auto v4 = GetNumber(1);
27    int x = 1;
28
29    // x is not a constant-expression, not constant-evaluated
30    const auto v5 = GetNumber(x);
31
32    assert(v4 == 1);
33    assert(v5 == 2);    
34}
35
36// pathological examples
37// always returns `true`
38constexpr bool IsInConstexpr(int){
39    if constexpr(std::is_constant_evaluated()){ // always `true`
40        return true;
41    }
42    return false;
43}
44
45// always returns `sizeof(int)`
46constexpr std::size_t GetArraySize(int){
47    int arr[std::is_constant_evaluated()];  // always int arr[1];
48    return sizeof(arr);
49}
50
51// always returns `1`
52constexpr std::size_t GetStdArraySize(int){
53    std::array<int, std::is_constant_evaluated()> arr;  // std::array<int, 1>
54    return arr.size();
55}

تجمیع‌ها

در این بخش با خصوصیات «تجمیع‌ها» (Aggregates) در C++20 آشنا می‌شویم.

ممنوعیت تجمیع در سازنده‌های اعلان شده از سوی کاربر

در نسخه جدید زبان ++C انواع تجمیع نمی‌توانند سازنده‌های اعلان شده از سوی کاربر داشته باشند. تا پیش از این تجمیع‌ها تنها مجاز به داشتن سازنده‌های حذف شده یا پیش‌فرض بودند. این امر موجب بروز رفتارهای عجیب در تجمیع‌های دارای سازنده‌های حذفی یا پیش‌فرضی می‌شد، چون این تجمیع‌ها از سوی کاربر اعلان می‌شدند اما در اختیار وی قرار نمی‌گرفتند.

1// none of the types below are an aggregate in C++20
2struct S{
3    int x{2};
4    S(int) = delete; // user-declared ctor
5};
6
7struct X{
8    int x;
9    X() = default;  // user-declared ctor
10};
11
12struct Y{
13    int x;
14    Y();            // user-provided ctor
15};
16
17Y::Y() = default;
18
19void f(){
20    S s(1);     // always an error
21    S s2{1};    // OK in C++17, error in C++20, S is not an aggregate now
22    X x{1};     // OK in C++17, error in C++20
23    Y y{2};     // always an error
24}

استنتاج آرگومان تمپلیت کلاس برای تجمیع‌ها

در C++17 برای استفاده از تجمیع با CTAD نیاز به راهنمای استنتاج صریح داشتیم که اکنون غیرضروری شده است:

1template<typename T, typename U>
2struct S{
3    T t;
4    U u;
5};
6// deduction guide was needed in C++17
7// template<typename T, typename U>
8// S(T, U) -> S<T,U>;
9
10S s{1, 2.0};    // S<int, double>

اکنون در صورت وجود راهنمایی‌های استنتاج ارائه شده از سوی کاربر دیگر نیازی به CTAD نداریم.

1template<typename T>
2struct MyData{
3    T data;
4};
5MyData(const char*) -> MyData<std::string>;
6
7MyData s1{"abc"};   // OK, MyData<std::string> using deduction guide
8MyData<int> s2{1};  // OK, explicit template argument
9MyData s3{1};       // Error, CTAD isn't involved

اینک می‌توان انواع آرایه را استنتاج کرد:

1template<typename T, std::size_t N>
2struct Array{
3    T data[N];
4};
5
6Array a{{1, 2, 3}}; // Array<int, 3>, notice additional braces
7Array str{"hello"}; // Array<char, 6>

حذف آکولاد در مورد انواع غیر آرایه‌ای وابسته یا انواع آرایه‌ای با کران وابسته پاسخگو نیست.

1template<typename T, typename U>
2struct Pair{
3    T first;
4    U second;
5};
6
7template<typename T, std::size_t N>
8struct A1{
9    T data[N];
10    T oneMore;
11    Pair<T, T> p;
12};
13
14template<typename T>
15struct A2{
16    T data[3];
17    T oneMore;
18    Pair<int, int> p;
19};
20
21// A1::data is an array of dependent bound and A1::p is a dependent type, thus,
22// no brace elision for them
23A1 a1{{1,2,3}, 4, {5, 6}};  // A1<int, 3>
24// A2::data is an array of non-dependent bound and A1::p is a non-dependent type,
25// thus, brace elision works
26A2 a2{1, 2, 3, 4, 5, 6};    // A2<int>

اما حذف آکولاد در مورد بسط پک‌ها کار می‌کند. عنصر با تجمیع متوالی که یک بسط پک هست با همه عناصر باقی‌مانده متناظر خواهد بود.

1template<typename... Ts>
2struct Overload : Ts...{
3    using Ts::operator()...;
4};
5// no need for deduction guide anymore
6
7Overload p{[](int){
8        std::cout << "called with int";
9    }, [](char){
10        std::cout << "called with char";
11    }
12};     // Overload<lambda(int), lambda(char)>
13p(1);   // called with int
14p('c'); // called with char

عنصر غیر متوالی که یک بسط پک باشد با هیچ عنصری متناظر نخواهد بود:

1template<typename T, typename...Ts>
2struct Pack : Ts... {
3    T x;
4};
5
6// can deduce only the first element
7Pack p1{1};         // Pack<int>
8Pack p2{[]{}};      // Pack<lambda()>
9Pack p3{1, []{}};   // error

تعداد عناصر در پک تنها یک بار استنتاج می‌شود، اما در صورت تکرار شدن، انواع باید دقیقاً منطبق باشند:

1struct A{};
2struct B{};
3struct C{};
4struct D{
5    operator C(){return C{};}
6};
7
8template<typename...Ts>
9struct P : std::tuple<Ts...>, Ts...{
10};
11
12P{std::tuple<A, B, C>{}, A{}, B{}, C{}}; // P<A, B, C>
13
14// equivalent to the above, since pack elements were deduced for
15// std::tuple<A, B, C> there's no need to repeat their types
16P{std::tuple<A, B, C>{}, {}, {}, {}}; // P<A, B, C>
17
18// since we know the whole P<A, B, C> type after std::tuple initializer, we can
19// omit trailing initializers, elements will be value-initialized as usual
20P{std::tuple<A, B, C>{}, {}, {}}; // P<A, B, C>
21
22// error, pack deduced from first initializer is <A, B, C> but got <A, B, D> for
23// the trailing pack, implicit conversions are not considered
24P{std::tuple<A, B, C>{}, {}, {}, D{}};

مقداردهی پرانتزی تجمیع‌ها

اکنون مقداردهی پرانتزی تجمیع‌ها به همان شیوه مقداردهی آکولادی عمل می‌کند، به جز این که تبدیل‌های کوچک‌سازی مجاز هستند، مقداردهی اختصاصی مجاز نیست، امکان بسط عمر مقادیر موقت وجود ندارد و حذف آکولادی نیز ممکن نیست. عناصر فاقد مقداردهنده به صورت «با مقدار» مقداردهی می‌شوند. به این ترتیب امکان استفاده هموار از تابع‌های فکتوری مانند ()std::make_unique<>()/emplace در تجمیع‌ها فراهم می‌آید.

1struct S{
2    int a;
3    int b = 2;
4    struct S2{
5        int d;
6    } c;
7};
8
9struct Ref{
10    const int& r;
11};
12
13int GetInt(){
14    return 21;
15}
16
17S{0.1}; // error, narrowing
18S(0.1); // OK
19
20S{.a=1}; // OK
21S(.a=1); // error, no designated initializers
22
23Ref r1{GetInt()}; // OK, lifetime is extended
24Ref r2(GetInt()); // dangling, lifetime is not extended
25
26S{1, 2, 3}; // OK, brace elision, same as S{1,2,{3}}
27S(1, 2, 3); // error, no brace elision
28
29// values without initializers take default values or value-initialized(T{})
30S{1}; // {1, 2, 0}
31S(1); // {1, 2, 0}
32
33// make_unique works now
34auto ps = std::make_unique<S>(1, 2, S::S2{3});
35
36// arrays are also supported
37int arr1[](1, 2, 3);
38int arr2[2](1); // {1, 0}

قابلیت‌های سی پلاس پلاس

پارامترهای تمپلیت غیر نوعی

در این بخش به بررسی خصوصیت پارامترهای قالبی غیر نوعی در C++20 می‌پردازیم.

انواع کلاس در پارامترهای تمپلیت غیر نوعی

پارامترهای تمپلیت غیر نوعی اکنون می‌توانند انواع کلاس لفظی باشند. این انواع قابلیت استفاده به صورت یک متغیر constexpr را با همه اعضای مبنا و غیر استاتیک به صورت public و غیر mutable دارند. وهله‌هایی از این کلاس‌ها به صورت اشیای const ذخیره می‌شوند و حتی امکان فراخوانی تابع‌های عضو آن‌ها نیز وجود دارد. یک نوع جدیدی از پارامتر تمپلیت غیر نوعی به صورت یک placeholder برای یک نوع کلاس استنتاجی وجود دارد. در مثال زیر fixed_string یک نام تمپلیت و نه یک نام نوع است، اما می‌توانیم از آن برای اعلان پارامتر تمپلیت template<fixed_string S> استفاده کنیم. در چنین مواردی کامپایلر آرگومان‌های پارامتر را برای fixed_string پیش از وهله‌سازی از f<>() با استفاده از یک اعلان ابداعی به شکل T x = template-argument; استنتاج می‌کند. روش استفاده از آن برای ایجاد یک کلاس ساده رشته زمان کامپایل چنین است:

1template<std::size_t N>
2struct fixed_string{
3    constexpr fixed_string(const char (&s)[N+1]) {
4        std::copy_n(s, N + 1, str);
5    }
6    constexpr const char* data() const {
7        return str;
8    }
9    constexpr std::size_t size() const {
10        return N;
11    }
12
13    char str[N+1];
14};
15
16template<std::size_t N>
17fixed_string(const char (&)[N])->fixed_string<N-1>;
18
19// user-defined literals are also supported
20template<fixed_string S>
21constexpr auto operator""_cts(){
22    return S;
23}
24
25// N for `S` will be deduced
26template<fixed_string S>
27void f(){
28    std::cout << S.data() << ", " << S.size() << '\n';
29}
30
31f<"abc">(); // abc, 3
32constexpr auto s = "def"_cts;
33f<s>();     // def, 3

پارامترهای تمپلیت غیر نوعی تعمیم‌یافته

پارامترهای تمپلیت غیر نوعی به صورت انواع مشهور به ساختاری تعمیم می‌یابند. «نوع ساختاری» (Structural type) یکی از موارد زیر است:

  • نوع اسکالر (حسابی، اشاره‌گر، اشاره‌گر به عضو، شمارشی، std::nullptr_t)
  • ارجاع lvalue
  • کلاس لفظی با مشخصه‌های زیر: همه کلاس‌های مبنا و اعضای داده‌ای غیر استاتیک پابلیک و غیر mutable باشند و انواعشان ساختاری یا انواع آرایه‌ای باشد.

به این ترتیب امکان استفاده از نوع اعشاری و انواع کلاس به صورت پارامترهای تمپلیت وجود دارد:

1template<auto T>    // placeholder for any non-type template parameter
2struct X{};
3
4template<typename T, std::size_t N>
5struct Arr{
6    T data[N];
7};
8
9X<5> x1;
10X<'c'> x2;
11X<1.2> x3;
12// with the help of CTAD for aggregates
13X<Arr{{1,2,3}}> x4; // X<Arr<int, 3>>
14X<Arr{"hi"}> x5;    // X<Arr<char, 3>>

نکته قابل توجه در این خصوص آن است که آرگومان‌های تمپلیت غیر نوعی نه با ()==operator خود بلکه به شیوه bitwise-like مقایسه می‌شوند. معنی این حرف آن است که بازنمایی بیتی آن‌ها برای مقایسه مورد استفاده قرا می‌گیرد. Union-ها استثنا هستند، زیرا کامپایلر می‌تواند اعضای فعالشان را پیگیری کند. دو یونیون زمانی با هم برابر هستند که هر دو عضو فعال نداشته باشند و یا عضو فعال یکسانی با مقدار برابر داشته باشند.

1template<auto T>
2struct S{};
3
4union U{
5    int a;
6    int b;
7};
8
9enum class E{
10    A = 0,
11    B = 0
12};
13
14struct C{
15    int x;
16    bool operator==(const C&) const{    // never equal
17        return false;
18    }
19};
20
21constexpr C c1{1};
22constexpr C c2{1};
23assert(c1 != c2);                           // not equal using operator==()
24assert(memcmp(&c1, &c2, sizeof(C)) == 0);   // but equal bitwise
25// thus, equal at compile-time, operator==() is not used
26static_assert(std::is_same_v<S<c1>, S<c2>>);
27
28constexpr E e1{E::A};
29constexpr E e2{E::B};
30// equal bitwise, enum's identity isn't taken into account
31assert(memcmp(&e1, &e2, sizeof(E)) == 0);
32static_assert(std::is_same_v<S<e1>, S<e2>>); // thus, equal at compile-time
33
34constexpr U u1{.a=1};
35constexpr U u2{.b=1};
36// equal bitwise but have different active members(a vs. b)
37assert(memcmp(&u1, &u2, sizeof(U)) == 0);
38// thus, not equal at compile-time
39static_assert(!std::is_same_v<S<u1>, S<u2>>);

اتصال‌های ساخت‌یافته

در این بخش به بررسی ویژگی «اتصال‌های ساخت‌یافته» (Structured bindings) در C++20 می‌پردازیم.

دریافت لامبدا و تصریح‌کننده‌های کلاس ذخیره‌سازی برای اتصال‌های ساخت‌یافته

اتصال‌های ساخت‌یافته مجاز به داشتن خصوصیت [[maybe_unused]] و تصریح‌کننده‌های static و thread_local هستند. همچنین اکنون امکان دریافت آن‌ها به صورت «با مقدار» و «با ارجاع» در لامبداها فراهم آمده است. توجه کنید که فیلدهای بیتی اتصال یافته تنها به صورت با مقدار می‌توانند دریافت شوند.

1struct S{
2    int a: 1;
3    int b: 1;
4    int c;
5};
6
7static auto [A,B,C] = S{};
8
9void f(){
10    [[maybe_unused]] thread_local auto [a,b,c] = S{};
11    auto l = [=](){
12        return a + b + c;
13    };
14
15    auto m = [&](){
16        // error, can't capture bit-fields 'a' and 'b' by-reference
17        // return a + b + c;
18        return c;
19    };
20}

آزادسازی سفارشی‌سازی اتصال‌های ساخت‌یافته

یکی از روش‌های تجزیه یک نوع برای اتصال ساخت‌یافته از طریق API «شبه چندتایی» (tuple-like) است. این API از سه تابع std::tuple_element، std::tuple_size و دو گزینه برای get به صورت ()<e.get<I یا get<I>(e) تشکیل یافته که اولی نسبت به دومی اولویت دارد. یعنی ()get عضو نسبت به نوع غیر عضوی ارجحیت دارد. نوعی را تصور کنید که دارای یک ()get است، اما نمی‌تواند تجزیه شود، زیرا کامپایلر تلاش خواهد کرد از ()get عضوی استفاده کند و پاسخ نخواهد گرفت. اکنون این مشکل به ترتیب اصلاح شده که نسخه عضوی تنها در صورتی اولویت داشته باشد که یک تمپلیت و پارامتر نخست تمپلیت به صورت پارامتر تمپلیت غیر نوعی باشند.

1struct X : private std::shared_ptr<int>{
2    std::string payload;
3};
4
5// due to new rules, this function is used instead of std::shared_ptr<int>::get
6template<int N>
7std::string& get(X& x) {
8    if constexpr(N==0) return x.payload;
9}
10
11namespace std {
12    template<>
13    class tuple_size<X> 
14        : public std::integral_constant<int, 1>
15    {};
16    
17    template<>
18    class tuple_element<0, X> {
19    public:
20        using type = std::string;
21    };
22}
23
24void f(){
25    X x;
26    auto& [payload] = x;
27}

اجازه اتصال ساخت‌یافته به اعضای دسترس‌پذیر

این قابلیت موجب می‌شود که اتصال‌های ساخت‌یافته نه تنها به اعضای Pubic بلکه به اعضای دسترس‌پذیر در ساختار اعلان اتصال ساخت‌یافته برقرار شوند.

1struct A {
2    friend void foo();
3private:
4    int i;
5};
6
7void foo() {
8    A a;
9    auto x = a.i;   // OK
10    auto [y] = a;   // Ill-formed until C++20, now OK
11}

حلقه for مبتنی بر بازه

در این بخش با یکی دیگر از قابلیت‌های جدید C++20 که با حلقه‌های for مرتبط است آشنا خواهیم شد.

گزاره‌های init برای ساخت حلقه‌های for مبتنی بر بازه

در C++20 حلقه‌های for مبتنی بر بازه نیز مانند گزاره if می‌توانند گزاره if داشته باشند. این گزاره می‌تواند برای جلوگیری از ارجاع‌های آویزان استفاده شود.

1class Obj{
2    std::vector<int>& GetItems();
3};
4
5Obj GetObj();
6
7// dangling reference, lifetime of Obj return by GetObj() is not extended
8for(auto x : GetObj().GetCollection()){
9    // ...
10}
11
12// OK
13for(auto obj = GetObj(); auto item : obj.GetCollection()){
14    // ...
15}
16
17// also can be used to maintain index
18for(std::size_t i = 0; auto& v : collection){
19    // use v...
20    i++;
21}

تعیین قواعدی برای آزادسازی نقطه سفارشی‌سازی حلقه for مبتنی بر بازه

این قابلیت همانند اصلاح نقطه سفارشی‌سازی اتصال‌های ساخت‌یافته است. برای چرخش روی یک بازه، حلقه for مبتنی بر بازه باید یا آزاد و یا یک تابع عضویت باشد. قواعد قدیمی به ترتیبی کار می‌کرد که اگر هر عضو (تابع یا متغیر) به نام begin/end پیدا می‌شد، کامپایلر می‌توانست از تابع‌های عضویت استفاده کند. این وضعیت موجب بروز مشکلاتی برای نوع‌هایی می‌شد که یک begin عضو داشتند، اما هیچ end نداشتند و یا در وضعیت برعکس بودند. اکنون تابع‌های عضویت تنها در صورتی مورد استفاده قرار می‌گیرند که هر دو نام موجود باشند و در غیر این صورت تابع‌های آزاد استفاده می‌شوند.

1struct X : std::stringstream {
2  // ...
3};
4
5std::istream_iterator<char> begin(X& x){
6    return std::istream_iterator<char>(x);
7}
8
9std::istream_iterator<char> end(X& x){
10    return std::istream_iterator<char>();
11}
12
13void f(){
14    X x;
15    // X has member with name `end` inherited from std::stringstream
16    // but due to new rules free begin()/end() are used
17    for (auto&& i : x) {
18        // ...
19    }
20}

خصوصیت‌ها

در این بخش با «خصوصیت‌ها» (Attributes) در C++20 آشنا می‌شویم.

[[likely]] و [[unlikely]]

خصوصیت‌های [[likely]] و [[unlikely]] سرنخی در مورد احتمال مسیر اجرایی به کامپایلر می‌دهند که به کامپایلر کمک می‌کند تا کد را به روش بهتری بهینه‌سازی کند. این خصوصیت‌ها می‌توانند روی گزاره‌ها از قبیل گزاره if یا حلقه‌ها و یا روی برچسب‌ها مانند case/default استفاده شوند.

1int f(bool b){
2    if(b) [[likely]] {
3        return 12;
4    }
5    else{
6        return 10;
7    }
8}
9

خصوصیت [[no_unique_address]]

خصوصیت  [[no_unique_address]] می‌تواند روی یک عضو داده‌ای غیر استاتیک غیر فیلدبیتی اعمال شود تا مشخص سازد که این عضو به یک نشانی یکتا نیاز ندارد. در عمل، این خصوصیت روی یک عضو داده‌ای بالقوه خالی اعمال می‌شود و کامپایلر می‌تواند آن را طوری بهینه‌سازی کند که هیچ فضایی اشغال نسازد. چین عضوی می‌تواند در نشانی عضو دیگر یا کلاس مبنا شریک شود.

1struct Empty{};
2
3template<typename T>
4struct Cpp17Widget{
5    int i;
6    T t;
7};
8
9template<typename T>
10struct Cpp20Widget{
11    int i;
12    [[no_unique_address]] T t;
13};
14
15static_assert(sizeof(Cpp17Widget<Empty>) > sizeof(int));
16static_assert(sizeof(Cpp20Widget<Empty>) == sizeof(int));

خصوصیت [[nodiscard]] با پیام

خصوصیت [[nodiscard]] نیز مانند [[deprecated("reason")]] می‌تواند یک دلیل داشته باشد.

1// test whether it's supported
2static_assert(__has_cpp_attribute(nodiscard) == 201907L);
3
4[[nodiscard("Don't leave me alone")]]
5int get();
6
7void f(){
8    get(); // warning: ignoring return value of function declared with 
9           // 'nodiscard' attribute: Don't leave me alone
10}

خصوصیت [[nodiscard]] برای سازنده‌ها

این خصوصیت به طور مشخص امکان به‌کارگیری [[nodiscard]] را روی سازنده‌ها فراهم می‌سازد. کامپایلرها تا پیش از C++20 الزامی به پشتیبانی از آن نداشتند.

1struct resource{
2    // empty resource, no harm if discarded
3    resource() = default;
4    
5    [[nodiscard("don't discard non-empty resource")]]
6    resource(int fd);
7};
8
9void f(){
10    resource{};     // OK
11    resource{1};    // warning
12}

انکودینگ کاراکتر

در این بخش با انکودینگ‌های مختلف C++20 آشنا می‌شویم.

char8_t

C++17 لفظ کاراکتری u8 را برای رشته‌های UTF-8 معرفی کرد، اما نوع آن char ساده بود. عدم امکان تمایز انکودینگ بر حسب یک نوع منجر به تولید کدی می‌شود که باید از ترفندهای مختلفی برای مدیریت انکودینگ‌های مختلف بهره جست. در C++20 یک نوع char8_t برای بازنمایی کاراکترهای UTF-8 معرفی شده است. این انکودینگ همان اندازه، علامت‌پذیری، جهت‌گیری و غیره را به اندازه unsigned char دارد، اما یک نوع متمایز است و «اسم مستعار» (Alias) محسوب نمی‌شود.

1void HandleString(const char*){}
2// distinct function name is required to handle UTF-8 in C++17
3void HandleStringUTF8(const char*){}
4// now it can be done using convenient overload
5void HandleString(const char8_t*){}
6
7void Cpp17(){
8    HandleString("abc");        // char[4]
9    HandleStringUTF8(u8"abc");  // C++17: char[4] but UTF-8, 
10                                // C++20: error, type is char8_t[4]
11}
12
13void Cpp20(){
14    HandleString("abc");    // char
15    HandleString(u8"abc");  // char8_t
16}

الزامات دقیق‌تر یونیکد

در نسخه جدید زبان سی پلاس پلاس انواع char16_t و char32_t به صورت صریح باید به ترتیب لفظ‌های رشته‌ای UTF-16 و UTF-32 را بازنمایی کنند. نام‌های سراسری کاراکتر یعنی \Unnnnnnnn و \uNNNN باید با نقاط کد ISO/IEC 10646 متناظر باشند و جایگزین نقاط کد دیگر نشوند، چون جز در این حالت برنامه شکل نامناسبی خواهد داشت.

1char32_t c{'\U00110000'};// error: invalid universal character

قابلیت‌های سی پلاس پلاس

برخی تغییرات ظاهری

مقداردهنده‌های اختصاصی

در نسخه جدید این زبان امکان مقداردهی اعضای تجمیع خاص (اختصاصی) و رد شدن از باقی موارد وجود دارد. برخلاف زبان C ترتیب مقداردهی باید همانند ترتیب اعلان تجمیع باشد:

1struct S{
2    int x;
3    int y{2};
4    std::string s;
5};
6S s1{.y = 3};   // {0, 3, {}}
7S s2 = {.x = 1, .s = "abc"};    // {1, 2, {"abc"}}
8S s3{.y = 1, .x = 2};   // Error, x should be initialized before y

مقداردهی‌های اعضای پیش‌فرض برای فیلدهای بیتی

تا C++20 برای عرضه مقدار پیش‌فرض برای یک فیلد بیتی شخص باید یک سازنده پیش‌فرض ایجاد می‌کرد، اما در نسخه جدید این زبان می‌توان از ساختار مقداردهی عضو پیش‌فرض که آسان‌تر است بهره گرفت.

1// until C++20:
2struct S{
3    int a : 1;
4    int b : 1;
5    S() : a{0}, b{1}{}
6};
7
8// since C++20:
9struct S{
10    int a : 1 {0},
11    int b : 1 = 1;
12};

ساخت typename با اختیار بیشتر

Typename را می‌توان در ساختارهایی که هیچ چیز به جز نام نوع ظاهر نخواهد شد، حذف کرد:

1template <class T>
2T::R f();  // OK, return type of a function declaration at global scope
3
4template <class T>
5void f(T::R);   // Ill-formed (no diagnostic required), attempt to declare a
6                // void variable template
7
8template<typename T>
9struct PtrTraits{
10    using Ptr = void*;
11};
12
13template <class T>
14struct S {
15  using Ptr = PtrTraits<T>::Ptr;  // OK, in a defining-type-id
16  T::R f(T::P p) {                // OK, class scope
17    return static_cast<T::R>(p);  // OK, type-id of a static_cast
18  }
19  auto g() -> S<T*>::Ptr; // OK, trailing-return-type
20
21  T::SubType t;
22};
23
24template <typename T>
25void f() {
26  void (*pf)(T::X); // Variable pf of type void* initialized with T::X
27  void g(T::X);     // Error: T::X at block scope does not denote a type
28                    // (attempt to declare a void variable)
29}

فضاهای نام inline تودرتو

اکنون کلیدواژه inline مجاز به استفاده در تعاریف تودرتوی فضا نام است:

1// C++20
2namespace A::B::inline C{
3    void f(){}
4}
5// C++17
6namespace A::B{
7    inline namespace C{
8        void f(){}
9    }
10}

استفاده از enum

شمارش‌های دامنه‌دار ابزار فوق‌العاده‌ای هستند. تنها مشکل آن‌ها این است که طول زیادی دارند: my_enum::enum_value. برای نمونه در گزاره switch که همه مقادیر ممکن enum بررسی می‌شود، بخش ::my_enum باید برای هر برچسب تکرار شود. با استفاده از اعلان enum می‌توان همه نام‌های شمارشی را در دامنه جاری وارد کرد تا به صورت نام‌های احراز نشده ظاهر شوند و به ای ترتیب می‌توان بخش ::my_enum را نادیده گرفت. این قاعده می‌تواند روی شمارش‌های بی‌دامنه و حتی روی یک شمارنده منفرد نیز اعمال شود.

1namespace my_lib {
2enum class color { red, green, blue };
3enum COLOR {RED, GREEN, BLUE};
4enum class side {left, right};
5}
6
7void f(my_lib::color c1, my_lib::COLOR c2){
8    using enum my_lib::color;   // introduce scoped enum
9    using enum my_lib::COLOR;   // introduce unscoped enum
10    using my_lib::side::left;   // introduce single enumerator id
11
12    // C++17
13    if(c1 == my_lib::color::red){/*...*/}
14    
15    // C++20
16    if(c1 == green){/*...*/}
17    if(c2 == BLUE){/*...*/}
18
19    auto r = my_lib::side::right;   // qualified id is required for `right`
20    auto l = left;                  // but not for `left`
21}

استنتاج اندازه ارائه در عبارت‌های new

این اصلاحیه موجب می‌شود که کامپایلر بتواند اندازه آرایه را در عبارت‌های new درست مانند روشی که در مورد متغیرهای لوکال عمل می‌کند تشخیص دهد:

1// before C++20
2int p0[]{1, 2, 3};
3int* p1 = new int[3]{1, 2, 3};  // explicit size is required
4
5// since C++20
6int* p2 = new int[]{1, 2, 3};
7int* p3 = new int[]{};  // empty
8char* p4 = new char[]{"hi"};
9// works with parenthesized initialization of aggregates
10int p5[](1, 2, 3);
11int* p6 = new int[](1, 2, 3);

استنتاج آرگومان تمپلیت کلاس برای تمپلیت‌های مستعار

اکنون CTAD با اسامی مستعار نوع نیز کار می‌کند:

1template<typename T>
2using IntPair = std::pair<int, T>;
3
4double d{};
5IntPair<double> p0{1, d};   // C++17
6IntPair p1{1, d};   // std::pair<int, double>
7IntPair p2{1, p1};  // std::pair<int, std::pair<int, double>>

Constinit

++C یک مشکل مشهور «ترتیب مقداردهی استاتیک» داشت که در زمان تعریف نشده بودن ترتیب مقداردهی متغیرهای ذخیره‌سازی استاتیک از واحد‌های ترجمه دیگر رخ می‌داد. متغیرهای دارای مقداردهی صفر/ثابت از بروی این مشکل جلوگیری می‌کنند زیرا در زمان کامپایل مقداردهی می‌شوند. اکنون constinit الزام می‌کند که در زمان کامپایل مقداردهی شود و برخلاف constexpr امکان استفاده از تخریب‌گر‌های غیر trivial نیز وجود دارد. کاربرد دوم constinit در اعلان‌های thread_local بدون مقداردهی است. در چنین مواردی constinit به کامپایلر اعلام می‌کند که متغیر قبلاً مقداردهی شده است، چون در غیر این صورت کامپایلر معمولاً کدی اضافه می‌کند که در صورت نیاز در هر کاربرد بررسی و مقداردهی کند.

1struct S {
2    constexpr S(int) {}
3    ~S(){}; // non-trivial
4};
5
6constinit S s1{42};  // OK
7constexpr S s2{42};  // error because destructor is not trivial
8
9// tls_definitions.cpp
10thread_local constinit int tls1{1};
11thread_local int tls2{2};
12
13// main.cpp
14extern thread_local constinit int tls1;
15extern thread_local int tls2;
16
17int get_tls1() {
18    return tls1;  // pure TLS access
19}
20
21int get_tls2() {
22    return tls2;  // has implicit TLS initialization code
23}

اعداد صحیح علامت‌دار مکمل دو هستند

معنی این حرف آن است که اعداد صحیح علامت‌دار در C++20 به طور تضمین‌شده مکمل دو هستند. به این ترتیب برخی رفتارهای تعریف نشده و تعریف شده بر اساس پیاده‌سازی حذف می‌شوند، زیرا بازنمایی دودویی اصلاح شده است. سرریز برای اعداد صحیح علامت‌دار همچنان UB است اما همین حالت هم اکنون خوش-تتعریف محسوب می‌شود.

1int i1 = -1;
2// left-shift for signed negative integers(previously undefined behavior)
3i1 <<= 1;    // -2
4
5int i2 = INT_MAX;
6// "unrepresentable" left-shift for signed integers(previously undefined behavior)
7i2 <<= 1;   // -2
8
9int i3 = -1;
10// right shift for signed negative integers, performs sign-extension(previously 
11// implementation-defined)
12i3 >>= 1;   // -1
13int i4 = 1;
14i4 >>= 1;   // 0
15
16// "unrepresentable" conversions to signed integers(previously implementation-defined)
17int i5 = UINT_MAX;  // -1

__VA_OPT__ برای ماکروهای متغیرپذیر

این قابلیت جدید سی پلاس پلاس امان مدیریت ساده‌تر «ماکروهای متغیرپذیر» (variadic macros) را فراهم ساخته است. اگر ماکرو خالی باشد به صورت __VA_ARGS__ بسط می‌یابد و در غیر این صورت به محتوای ماکرو بسط خواهد یافت. این قابلیت به طور خاص زمانی مفید است که ماکرو یک تابع را با برخی آرگومان (های) از پیش تعریف شده و در ادامه با __VA_ARGS__ اختیاری فرا بخواند. در چنین مواردی اگر __VA_ARGS__ خالی باشد، __VA_OPT__ امکان حذف کامای انتهایی را فراهم می‌سازد.

1#define LOG1(...)                   \
2    __VA_OPT__(std::printf(__VA_ARGS);) \
3    std::printf("\n");
4
5LOG1();                      // std::printf("\n");
6LOG1("number is %d", 12);    // std::printf("number is %d", 12); std::printf("\n");
7
8#define LOG2(msg, ...) \
9    std::printf("[" __FILE__ ":%d] " msg, __LINE__, __VA_ARGS__)
10#define LOG3(msg, ...) \
11    std::printf("[" __FILE__ ":%d] " msg, __LINE__ __VA_OPT__(,) __VA_ARGS__)
12
13// OK, std::printf("[" "file.cpp" ":%d] " "%d errors.\n", 14, 0);
14LOG2("%d errors\n", 0);
15
16// Error, std::printf("[" "file.cpp" ":%d] " "No errors\n", 17, );
17LOG2("No errors\n");
18
19// OK, std::printf("[" "file.cpp" ":%d] " "No errors\n", 20);
20LOG3("No errors\n");

تابع‌های پیش‌فرضی صریح با مشخصه‌های استثنای متفاوت

این قابلیت امکان تعیین مشخصه‌های استثنای یک تابع صریحاً پیش‌فرضی را به روشی متفاوت از مشخصه‌های تابع با اعلان ضمنی فراهم می‌سازد. تا C++20 چنین اعلان‌هایی موجب خروج برنامه از حالت خوش-تعریف می‌شدند. اکنون این وضعیت مجاز است و البته مشخصه استثنا را در به صورت عملی ارائه می‌کند. این حالت در مواردی مفید است که بخواهیم noexcept بودن برخی انواع عملیات‌ را الزام کنیم. برای نمونه به جهت تضمین قوی استثنا، std::vector عناصرش را تنها در صورتی به یک فضای ذخیره‌سازی جدید منتقل می‌کند که سازنده‌های جابجایی آن noexcept باشند و در غیر این صورت عناصر کپی می‌شوند. برخی اوقات این پیاده‌سازی سریع‌تر حتی در صورتی که عنصر در طی جابجایی حذف شوند، برای ما مطلوب خواهد بود. به طور معمول وقتی یک تابع به صورت noexcept نشانه‌گذاری شده باشد، ()std::terminate فراخوانی می‌شود.

1struct S1{
2    // ill-formed until C++20 because implicit constructor is noexcept(true)
3    S1(S1&&)noexcept(false) = default; // can throw
4};
5
6struct S2{
7    S2(S2&&) noexcept = default;
8    // implicitly generated move constructor would be `noexcept(false)`
9    // because of `s1`, now it's enforced to be `noexcept(true)`
10    S1 s1;
11};
12
13static_assert(std::is_nothrow_move_constructible_v<S1> == false);
14static_assert(std::is_nothrow_move_constructible_v<S2> == true);
15
16struct X1{
17    X1(X1&&) noexcept = default;
18    std::map<int, int> m;   // `std::map(std::map&&)` can throw
19};
20
21struct X2{
22    // same as implicitly generated, it's `noexcept(false)` because of `std::map`
23    X2(X2&&) = default;
24    std::map<int, int> m;   // `std::map(std::map&&)` can throw
25};
26
27std::vector<X1> v1;
28std::vector<X2> v2;
29// ... at some point, `push_back()` needs to reallocate storage
30
31// efficiently uses `X1(X1&&)` to move the elements to a new storage,
32// calls `std::terminate()` if it throws
33v1.push_back(X1{});
34
35// uses `X2(const X2&)`, thus, copies, not moves elements to a new storage
36v2.push_back(X2{});

تخریب operator delete

C++20 یک عملگر ()operator delete خاص کلاس را معرفی کرده که یک تگ اختصاصی به صورت std::destroying_delete_t می‌گیرد. در چنین حالتی کامپایلر تخریب‌گر شیء را پیش از فراخوانی ()operator delete فراخوانی نمی‌کند و باید به صورت دستی فراخوانی شود. این وضعیت در مواردی مفید خواهد بود که لازم باشد اعضای شیء که برای استخراج اطلاعات استفاده می‌شوند برای خالی شدن حافظه اشغالی، آزادسازی شوند. چنین وضعیتی برای نمونه برای استخراج اندازه معتبر و فراخوانی نسخه اندازه‌بندی‌شده delete مفید است.

1struct TrickyObject{
2    void operator delete(TrickyObject *ptr, std::destroying_delete_t){
3        // without destroying_delete_t object would have been destroyed here
4        const std::size_t realSize = ptr->GetRealSizeSomehow();
5        // now we need to call the destructor by-hand
6        ptr->~TrickyObject();
7        // and free storage it occupies
8        ::operator delete(ptr, realSize);
9    }
10    // ...
11};

قابلیت‌های سی پلاس پلاس

سازنده‌های explicit شرطی

اکنون همانند noexcept(bool) یک سازنده به شکل explicit(bool) داریم که امکان ساخت/تبدیل شرطی explicit را فراهم می‌سازد.

1template<typename T>
2struct S{
3    explicit(!std::is_convertible_v<T, int>) S(T){}
4};
5
6void f(){
7    S<char> sc = 'x';           // OK
8    S<std::string> ss1 = "x";   // Error, constructor is explicit
9    S<std::string> ss2{"x"};    // OK
10}

ماکروهای تست قابلیت

C++20 یک مجموعه ماکروهای پیش‌پردازشگر برای تست قابلیت‌های مختلف زبان و کتابخانه ارائه کرده است که فهرست کامل آن را می‌توانید در این نشانی (+) ببینید.

1#ifdef __has_cpp_attribute  // check __has_cpp_attribute itself before using it
2#   if __has_cpp_attribute(no_unique_address) >= 201803L
3#       define CXX20_NO_UNIQUE_ADDR [[no_unique_address]]
4#   endif
5#endif
6
7#ifndef CXX20_NO_UNIQUE_ADDR
8#   define CXX20_NO_UNIQUE_ADDR
9#endif
10
11template<typename T>
12class Widget{
13    int x;
14    CXX20_NO_UNIQUE_ADDR T obj;
15};

تبدیل آرایه با کران مشخص به کران نامشخص

در نسخه جدید سی پلاس پلاس امکان تبدیل آرایه با کران‌های مشخص به ارجاعی به آرایه با کران نامشخص وجود دارد. قواعد تصمیم‌گیری Overload نیز طوری به‌روزرسانی شده است که اورلود با اندازه تطبیقی بهتر از اورلود ب اندازه نامشخص یا غیر تطبیقی اجرا شود.

1void f(int (&&)[]){};
2void f(int (&)[1]){};
3
4void g() {
5  int arr[1];
6
7  f(arr);       // calls `f(int (&)[1])`
8  f({1, 2});    // calls `f(int (&&)[])`
9  int(&r)[] = arr;
10}

انتقال صریح برای اشیای لوکال ماکرو و ارجاع‌های rvalue

در برخی حالت‌های خاص کامپایلر مجاز به جایگزینی کپی با انتقال است. اما در ادامه مشخص شد که قواعد بیش از اندازه محدودکننده است. C++17 امکان جابجایی ارجاع‌های rvalue را در گزاره‌های return، پارامترهای تابع در عبارت throw و شکل‌های مختلف تبدیل نمی‌دهد. C++20 این مشکلات را رفع کرده است، اما همچنان برخی موارد باقی مانده است.

1std::unique_ptr<T> f0(std::unique_ptr<T> && ptr) {
2    return ptr; // copied in C++17(thus, error), moved in C++20, OK
3}
4
5std::string f1(std::string && x) {
6    return x;   // copied in C++17, moved in C++20
7}
8
9struct Widget{};
10
11void f2(Widget w){
12    throw w;    // copied in C++17, moved in C++20
13}
14
15struct From {
16    From(Widget const &);
17    From(Widget&&);
18};
19
20struct To {
21    operator Widget() const &;
22    operator Widget() &&;
23};
24
25From f3() {
26    Widget w;
27    return w;  // moved (no NRVO because of different types)
28}
29
30Widget f4() {
31    To t;
32    return t;// copied in C++17(conversions were not considered), moved in C++20
33}
34
35struct A{
36    A(const Widget&);