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

آخرین به‌روزرسانی: ۱۷ خرداد ۱۴۰۰
زمان مطالعه: ۴۷ دقیقه
قابلیت‌های سی پلاس پلاس

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

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

مفاهیم کلی

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

عبارت Requires

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

template<typename T> /*...*/
requires (T x) // optional set of fictional parameter(s)
{
    // simple requirement: expression must be valid
    x++;    // expression must be valid
    
    // type requirement: `typename T`, T type must be a valid type
    typename T::value_type;
    typename S<T>;

    // compound requirement: {expression}[noexcept][-> Concept];
    // {expression} -> Concept<A1, A2, ...> is equivalent to
    // requires Concept<decltype((expression)), A1, A2, ...>
    {*x};  // dereference must be valid
    {*x} noexcept;  // dereference must be noexcept
    // dereference must  return T::value_type
    {*x} noexcept -> std::same_as<typename T::value_type>;
    
    // nested requirement: requires ConceptName<...>;
    requires Addable<T>; // constraint Addable<T> must be satisfied
};

کانسپت

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

template<typename T>
concept Addable = requires(T a, T b)
{
    a + b;
};

template<typename T>
concept Dividable = requires(T a, T b)
{
    a/b;
};

template<typename T>
concept DivAddable = Addable<T> && Dividable<T>;

template<typename T>
void f(T x)
{
    if constexpr(Addable<T>){ /*...*/ }
    else if constexpr(requires(T a, T b) { a + b; }){ /*...*/ }
}

بند Requires

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

template<typename T>
requires Addable<T>
auto f1(T a, T b) requires Subtractable<T>; // Addable<T> && Subtractable<T>

auto l = []<typename T> requires Addable<T>
    (T a, T b) requires Subtractable<T>{};

template<typename T>
requires Addable<T>
class C;

// infamous `requires requires`. First `requires` is requires-clause,
// second one is requires-expression. Useful if you don't want to introduce new
// concept.
template<typename T>
requires requires(T a, T b) {a + b;}
auto f4(T x);

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

template<Addable T>
void f();

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

template<typename T>
concept Integral = std::integral<T>;

template<typename T>
concept Integral4 = std::integral<T> && sizeof(T) == 4;

// requires-clause also works here
template<template<typename T1> requires Integral<T1> typename T>
void f2(){}

// f() and f2() forms are equal
template<template<Integral T1> typename T>
void f(){
    f2<T>();
}

// unconstrained template template parameter can accept constrained arguments
template<template<typename T1> typename T>
void f3(){}

template<typename T>
struct S1{};

template<Integral T>
struct S2{};

template<Integral4 T>
struct S3{};

void test(){
    f<S1>();    // OK
    f<S2>();    // OK
    // error, S3 is constrained by Integral4 which is more constrained than
    // f()'s Integral
    f<S3>();

    // all are OK
    f3<S1>();
    f3<S2>();
    f3<S3>();
}

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

template<typename T>
struct X{
    void f() requires std::integral<T>
    {}
};

void f(){
    X<double> x;
    x.f();  // error
    auto pf = &X<double>::f;    // error
}

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

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

template<typename T>
concept is_sortable = true;

auto l = [](auto x){};
void f1(auto x){}               // unconstrained template
void f2(is_sortable auto x){}   // constrained template

template<is_sortable auto NonTypeParameter, is_sortable TypeParameter>
is_sortable auto f3(is_sortable auto x, auto y)
{
    // notice that nothing is allowed between constraint name and `auto`
    is_sortable auto z = 0;
    return 0;
}

template<is_sortable auto... NonTypePack, is_sortable... TypePack>
void f4(TypePack... args){}

int f();

// takes two parameters
template<typename T1, typename T2>
concept C = true;
// binds second parameter
C<double> auto v = f(); // means C<int, double>

struct X{
    operator is_sortable auto() {
        return 0;
    }
};

auto f5() -> is_sortable decltype(auto){
    f4<1,2,3>(1,2,3);
    return new is_sortable auto(1);
}

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

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

template<typename T>
concept integral_or_floating = std::integral<T> || std::floating_point<T>;

template<typename T>
concept integral_and_char = std::integral<T> && std::same_as<T, char>;

void f(std::integral auto){}        // #1
void f(integral_or_floating auto){} // #2
void f(std::same_as<char> auto){}   // #3

// calls #1 because std::integral is more constrained
// than integral_or_floating(#2)
f(int{});
// calls #2 because it's the only one whose constraint is satisfied
f(double{});
// error, #1, #2 and #3's constraints are satisfied but unordered
// because std::same_as<char> appears only in #3
f(char{});

void f(integral_and_char auto){}    // #4

// calls #4 because integral_and_char is more
// constrained than std::same_as<char>(#3) and std::integral(#1)
f(char{});

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

template<typename T>
requires std::is_integral_v<T>  // uses type traits instead of concepts
void f1(){}  // #1

template<typename T>
requires std::is_integral_v<T> || std::is_floating_point_v<T>
void f1(){}  // #2

// error, #1 and #2 have common `std::is_integral_v<T>` expression
// but at different locations(line 2 vs. line 6), thus, #1 and #2 constraints
// are unordered and the call is ambiguous
f1(int{});

template<typename T>
concept C1 = requires{      // requires-expression is not decomposed
    requires std::integral<T>;
};

template<typename T>
concept C2 = requires{      // requires-expression is not decomposed
    requires (std::integral<T> || std::floating_point<T>);
};

void f2(C1 auto){}  // #3
void f2(C2 auto){}  // #4

// error, since requires-expressions are not decomposed, #3 and #4 have
// completely unrelated and hence unordered constraints and the call is
// ambiguous
f2(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 انتخاب می‌شود.

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

template<typename T>
class optional{
public:
    optional() = default;

    // trivial copy-constructor
    optional(const optional&) = default;

    // non-trivial copy-constructor
    optional(const optional& rhs)
        requires(!std::is_trivially_copy_constructible_v<T>){
        // ...
    }

    // trivial destructor
    ~optional() = default;

    // non-trivial destructor
    ~optional() requires(!std::is_trivial_v<T>){
        // ...
    }
    // ...
private:
    T value;
};

static_assert(std::is_trivial_v<optional<int>>);
static_assert(!std::is_trivial_v<optional<std::string>>);

ماژول‌ها

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

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

// module.cpp
// dots in module name are for readability purpose, they have no special meaning
export module my.tool;  // module declaration

export void f(){}       // export f()
void g(){}              // but not g()

// client.cpp
import my.tool;

f();    // OK
g();    // error, not exported

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

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

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

// tool.cpp
export module tool; // primary module interface unit
export import :helpers; // re-export(see below) helpers partition

export void f();
export void g();

// tool.internals.cpp
module tool:internals;  // implementation partition
void utility();

// tool.impl.cpp
module tool;    // implementation unit, implicitly imports primary module unit
import :internals;

void utility(){}

void f(){
    utility();
}

// tool.impl2.cpp
module tool;    // another implementation unit
void g(){}

// tool.helpers.cpp
export module tool:helpers; // module interface partition
import :internals;

export void h(){
    utility();
}

// client.cpp
import tool;

f();
g();
h();

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

اکسپورت

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

// tool.cpp
module tool;
export import :helpers; // import and re-export helpers interface partition

export int x{}; // export single declaration

export{         // export multiple declarations
    int y{};
    void f(){};
}

export namespace A{ // export the whole namespace
    void f();
    void g();
}

namespace B{
    export void f();// export a single declaration within a namespace
    void g();
}

namespace{
    export int x;   // error, x has internal linkage
    export void f();// error, f() has internal linkage
}

export class C; // export as incomplete type
class C{};
export C get_c();

// client.cpp
import tool;

C c1;    // error, C is incomplete
auto c2 = get_c();  // OK

ایمپورت

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

// tool.cpp
export module tool;
import :helpers;  // import helpers partition

export void f(){}

// tool.helpers.cpp
export module tool:helpers;

export void g(){}

// client.cpp
import tool;

f();
g();

واحدهای هدر

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

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

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

// header.h
#pragma once
class A{};
void g(){}

// tool.cpp
module;             // global module fragment
#include "header.h"
export module tool; // ends here

export void f(){    // uses declarations from header.h
    g();
    A a;
}

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

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

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

export module tool; // interface

export void f();    // declared here

module :private;    // implementation details

void f(){}          // defined here

حذف inline ضمنی

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

// header.h
struct C{
    void f(){}  // still inline because attached to a global module
};

// tool.cpp
module;
#include "header.h"

export module tool;

class A{};  // not exported

export struct B{// B is attached to module "tool"
    void f(){   // not implicitly inline anymore
        A a;    // can safely use non-exported name
    }

    inline void g(){
        A a;    // oops, uses non-exported name
    }

    inline void h(){
        f();    // fine, f() is not inline
    }
};

// client.cpp
import tool;

B b;
b.f();  // OK
b.g();  // error, A is undefined
b.h();  // OK

کوروتین‌ها

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

cppcoro::task<int> someAsyncTask()
{
    int result;
    // get the result somehow
    co_return result;
}

// task<> is analog of void for normal function
cppcoro::task<> usageExample()
{
    // creates a new task but doesn't start executing the coroutine yet
    cppcoro::task<int> myTask = someAsyncTask();
    // ...
    // Coroutine is only started when we later co_await the task.
    auto result = co_await myTask;
}

// will lazily generate numbers from 0 to 9
cppcoro::generator<std::size_t> getTenNumbers()
{
    std::size_t n{0};
    while (n != 10)
    {
        co_yield n++;
    }
}

void printNumbers()
{
    for(const auto n : getTenNumbers())
    {
        std::cout << n;    
    }
}

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

تا پیش از 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 نوشت و همه شش عملگر مقایسه‌ای را با معناشناسی عضویت‌گونه به دست آورد.

template<typename T1, typename T2>
void TestComparisons(T1 a, T2 b)
{
    (a < b), (a <= b), (a > b), (a >= b), (a == b), (a != b);
}

struct S2
{
    int a;
    int b;
};

struct S1
{
    int x;
    int y;
    // support homogeneous comparisons
    auto operator<=>(const S1&) const = default;
    // this is required because there's operator==(const S2&) which prevents
    // implicit declaration of defaulted operator==()
    bool operator==(const S1&) const = default;

    // support heterogeneous comparisons
    std::strong_ordering operator<=>(const S2& other) const
    {
        if (auto cmp = x <=> other.a; cmp != 0)
            return cmp;
        return y <=> other.b;
    }

    bool operator==(const S2& other) const
    {
        return (*this <=> other) == 0;
    }
};

TestComparisons(S1{}, S1{});
TestComparisons(S1{}, S2{});
TestComparisons(S2{}, S1{});

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

template<typename T>
struct X
{
    friend constexpr std::partial_ordering operator<=>(X, X) requires(sizeof(T) != 1) = default;
    // implicitly declares:
    // friend constexpr bool operator==(X, X) requires(sizeof(T) != 1) = default;

    [[nodiscard]] virtual std::strong_ordering operator<=>(const X&) const = default;
    // implicitly declares:
    //[[nodiscard]] virtual bool operator==(const X&) const = default; 
};

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

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

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

struct S4
{
    int x;
    int y;
    // member version must have op(const T&) const; form
    auto operator<=>(const S3&) const = default;

    // friend version can take arguments by const-reference or by-value
    // friend auto operator<=>(const S3&, const S3&) = default;
    // friend auto operator<=>(S3, S3) = default;
};

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

struct S5
{
    int x;
    std::strong_ordering operator<=>(const S5&) const;
    bool operator==(const S5&) const;
};

std::strong_ordering S5::operator<=>(const S5&) const = default;
bool S5::operator==(const S5&) const = default;

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

// not in our immediate control
struct Legacy
{
    bool operator==(Legacy const&) const;
    bool operator<(Legacy const&) const;
};

struct S6
{
    int x;
    Legacy l;
    // deleted because Legacy doesn't have operator<=>(), comparison category
    // can't be deduced
    auto operator<=>(const S6&) const = default;
};

struct S7
{
    int x;
    Legacy l;

    std::strong_ordering operator<=>(const S7& rhs) const = default;
    /*
    Since comparison category is provided explicitly, ordering can be
    synthesized using operator<() and operator==(). They must return exactly
    `bool` for this to work. It will work for weak and partial ordering as well.
    
    Here's an example of synthesized operator<=>():
    std::strong_ordering operator<=>(const S7& rhs) const
    {
        // use operator<=>() for int
        if(auto cmp = x <=> rhs.x; cmp != 0) return cmp;

        // synthesize ordering for Legacy using operator<() and operator==()
        if(l == rhs.l) return std::strong_ordering::equal;
        if(l < rhs.l) return std::strong_ordering::less;
        return std::strong_ordering::greater;
    }
    */
};

struct NoEqual
{
    bool operator<(const NoEqual&) const = default;
};

struct S8
{
    NoEqual n;
    // deleted, NoEqual doesn't have operator<=>()
    // auto operator<=>(const S8&) const = default;

    // deleted as well because NoEqual doesn't have operator==()
    std::strong_ordering operator<=>(const S8&) const = default;
};

struct W
{
    std::weak_ordering operator<=>(const W&) const = default;
};

struct S9
{
    W w;
    // ask for strong_ordering but W can provide only weak_ordering, this will
    // yield an error during instantiation
    std::strong_ordering operator<=>(const S9&) const = default;
    void f()
    {
        (S9{} <=> S9{});    // error
    }
};

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

struct S4
{
    int& r;
    // deleted because of reference member
    auto operator<=>(const S4&) const = default;
};

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

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

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

struct S

struct S{
    void f(){
        [=]{};          // captures this by reference, deprecated since C++20
        [=, *this]{};   // OK since C++17, captures this by value
        [=, this]{};    // OK since C++20, captures this by reference
    }
};

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

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

// lambda that expect std::vector<T>
// until C++20:
[](auto vector){
    using T =typename decltype(vector)::value_type;
    // use T
};
// since C++20:
[]<typename T>(std::vector<T> vector){
    // use T
};

// access argument type
// until C++20
[](const auto& x){
    using T = std::decay_t<decltype(x)>;
    // using T = decltype(x); // without decay_t<> it would be const T&, so
    T copy = x;               // copy would be a reference type
    T::static_function();     // and these wouldn't work at all
    using Iterator = typename T::iterator;
};
// since C++20
[]<typename T>(const T& x){
    T copy = x;
    T::static_function();
    using Iterator = typename T::iterator;
};

// perfect forwarding
// until C++20:
[](auto&&... args){
    return f(std::forward<decltype(args)>(args)...);
};
// since C++20:
[]<typename... Ts>(Ts&&... args){
    return f(std::forward<Ts>(args)...);
};

// and of course you can mix them with auto-parameters
[]<typename T>(const T& a, auto b){};

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

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

using L = decltype([]{});   // lambdas have no linkage
L PublicApi();              // L can't be used for external linkage

// in template , two different declarations
template<class T> void f(decltype([]{}) (*s)[sizeof(T)]);
template<class T> void f(decltype([]{}) (*s)[sizeof(T)]);

// again, lambda types are never equivalent
static decltype([]{}) f();
static decltype([]{}) f(); // error, return type mismatch

static decltype([]{}) g();
static decltype(g()) g(); // okay, redeclaration

// each specialization has its own lambda with unique type
template<typename T>
using R = decltype([]{});

static_assert(!std::is_same_v<R<int>, R<char>>);

// Lambda-based SFINAE and constraints are not supported, it just fails
template <class T>
auto f(T) -> decltype([]() { T::invalid; } ());
void f(...);

template<typename T>
void g(T) requires requires{
    [](){typename T::invalid x;}; }
{}
void g(...){}

f(0);  // error
g(0);  // error

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

// a.h
template<typename T>
int counter(){
    static int value{};
    return value++;
}

inline int f(){
    return counter<decltype([]{})>();
}

template<auto> struct S{ void call(){} };
// cast lambda to pointer
inline S<+[]{}> g_s;

// a.cpp
#include "a.h"
auto v = f();
g_s.call();

// b.cpp
#include "a.h"
auto v = f();
g_s.call();

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

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

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

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

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

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

void g(int, int){}

// C++17
template<class F, class... Args>
auto delay_apply(F&& f, Args&&... args) {
    return [f=std::forward<F>(f), tup=std::make_tuple(std::forward<Args>(args)...)]()
            -> decltype(auto) {
        return std::apply(f, tup);
    };
}

// C++20
template<typename F, typename... Args>
auto delay_call(F&& f, Args&&... args) {
    return [f = std::forward<F>(f), ...f_args=std::forward<Args>(args)]()
            -> decltype(auto) {
        return f(f_args...);
    };
}

void f(){
    delay_call(g, 1, 2)();
}

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

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

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

consteval int GetInt(int x){
    return x;
}

constexpr void f(){
    auto x1 = GetInt(1);
    constexpr auto x2 = GetInt(x1); // error x1 is not a constant-expression
}

تابع مجازی constexpr

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

struct Base{
    constexpr virtual ~Base() = default;
    virtual int Get() const = 0;    // non-constexpr
};

struct Derived1 : Base{
    constexpr int Get() const override {
        return 1;
    }
};

struct Derived2 : Base{
    constexpr int Get() const override {
        return 2;
    }
};

constexpr auto GetSum(){
    const Derived1 d1;
    const Derived2 d2;
    const Base* pb1 = &d1;
    const Base* pb2 = &d2;

    return pb1->Get() + pb2->Get();
}

static_assert(GetSum() == 1 + 2);   // evaluated at compile-time

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

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

constexpr void f(){
    try{
        auto p = new int;
        // ...
        delete p;
    }
    catch(...){     // ignored at compile-time
        // ...
    }
}

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

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

struct Base1{
    virtual ~Base1() = default;
    constexpr virtual int get() const = 0;
};

struct Derived1 : Base1{
    constexpr int get() const override {
        return 1;
    }
};

struct Base2{
    virtual ~Base2() = default;
    constexpr virtual int get() const = 0;
};

struct Derived2 : Base2{
    constexpr int get() const override {
        return 2;
    }
};

template<typename Base, typename Derived>
constexpr auto downcasted_get(){
    const Derived d;
    const Base& upcasted = d;
    const auto& downcasted = dynamic_cast<const Derived&>(upcasted);

    return downcasted.get();
}

static_assert(downcasted_get<Base1, Derived1>() == 1);
static_assert(downcasted_get<Base2, Derived2>() == 2);

// compile-time error, cannot cast Derived1 to Base2
static_assert(downcasted_get<Base2, Derived1>() == 1);

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

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

union Foo {
  int i;
  float f;
};

constexpr int f() {
  Foo foo{};
  foo.i = 3;    // i is an active member
  foo.f = 1.2f; // valid since C++20, f becomes an active member

//   return foo.i;  // error, reading inactive union member
  return foo.f;
}

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

تخصیص‌های constexpr

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

constexpr auto get_str()
{
    std::string s1{"hello "};
    std::string s2{"world"};
    std::string s3 = s1 + s2;
    return s3;
}

constexpr auto get_array()
{
    constexpr auto N = get_str().size();
    std::array<char, N> arr{};
    std::copy_n(get_str().data(), N, std::begin(arr));
    return arr;
}

static_assert(!get_str().empty());

// error because it holds data allocated at compile-time
constexpr auto str = get_str();

// OK, string is stored in std::array<char>
constexpr auto result = get_array();

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

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

struct NonTrivial{
    bool b = false;
};

struct Trivial{
    bool b;
};

template <typename T>
constexpr T f1(const T& other) {
    T t;        // default initialization
    t = other;
    return t;
}

template <typename T>
constexpr auto f2(const T& other) {
    T t;
    return t.b;
}

void test(){
    constexpr auto a = f1(Trivial{});   // error in C++17, OK in C++20
    constexpr auto b = f1(NonTrivial{});// OK

    constexpr auto c = f2(Trivial{}); // error, uninitialized Trivial::b is used
    constexpr auto d = f2(NonTrivial{}); // OK
}

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

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

constexpr int add(int a, int b){
    if (std::is_constant_evaluated()){
        return a + b;
    }
    else{
        asm("asm magic here");
        //...
    }
}

تابع std::is_constant_evaluated()

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

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

constexpr int GetNumber(){
    if(std::is_constant_evaluated()){   // should not be `if constexpr`
        return 1;
    }
    return 2;
}

constexpr int GetNumber(int x){
    if(std::is_constant_evaluated()){   // should not be `if constexpr`
        return x;
    }
    return x+1;
}

void f(){
    constexpr auto v1 = GetNumber();
    const auto v2 = GetNumber();

    // initialization of a non-const variable, not constant-evaluated
    auto v3 = GetNumber();

    assert(v1 == 1);
    assert(v2 == 1);
    assert(v3 == 2);

    constexpr auto v4 = GetNumber(1);
    int x = 1;

    // x is not a constant-expression, not constant-evaluated
    const auto v5 = GetNumber(x);

    assert(v4 == 1);
    assert(v5 == 2);    
}

// pathological examples
// always returns `true`
constexpr bool IsInConstexpr(int){
    if constexpr(std::is_constant_evaluated()){ // always `true`
        return true;
    }
    return false;
}

// always returns `sizeof(int)`
constexpr std::size_t GetArraySize(int){
    int arr[std::is_constant_evaluated()];  // always int arr[1];
    return sizeof(arr);
}

// always returns `1`
constexpr std::size_t GetStdArraySize(int){
    std::array<int, std::is_constant_evaluated()> arr;  // std::array<int, 1>
    return arr.size();
}

تجمیع‌ها

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

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

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

// none of the types below are an aggregate in C++20
struct S{
    int x{2};
    S(int) = delete; // user-declared ctor
};

struct X{
    int x;
    X() = default;  // user-declared ctor
};

struct Y{
    int x;
    Y();            // user-provided ctor
};

Y::Y() = default;

void f(){
    S s(1);     // always an error
    S s2{1};    // OK in C++17, error in C++20, S is not an aggregate now
    X x{1};     // OK in C++17, error in C++20
    Y y{2};     // always an error
}

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

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

template<typename T, typename U>
struct S{
    T t;
    U u;
};
// deduction guide was needed in C++17
// template<typename T, typename U>
// S(T, U) -> S<T,U>;

S s{1, 2.0};    // S<int, double>

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

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

MyData s1{"abc"};   // OK, MyData<std::string> using deduction guide
MyData<int> s2{1};  // OK, explicit template argument
MyData s3{1};       // Error, CTAD isn't involved

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

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

Array a{{1, 2, 3}}; // Array<int, 3>, notice additional braces
Array str{"hello"}; // Array<char, 6>

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

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;
};

// A1::data is an array of dependent bound and A1::p is a dependent type, thus,
// no brace elision for them
A1 a1{{1,2,3}, 4, {5, 6}};  // A1<int, 3>
// A2::data is an array of non-dependent bound and A1::p is a non-dependent type,
// thus, brace elision works
A2 a2{1, 2, 3, 4, 5, 6};    // A2<int>

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

template<typename... Ts>
struct Overload : Ts...{
    using Ts::operator()...;
};
// no need for deduction guide anymore

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

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

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

// can deduce only the first element
Pack p1{1};         // Pack<int>
Pack p2{[]{}};      // Pack<lambda()>
Pack p3{1, []{}};   // error

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

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

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

P{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{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{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{std::tuple<A, B, C>{}, {}, {}, D{}};

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

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

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

struct Ref{
    const int& r;
};

int GetInt(){
    return 21;
}

S{0.1}; // error, narrowing
S(0.1); // OK

S{.a=1}; // OK
S(.a=1); // error, no designated initializers

Ref r1{GetInt()}; // OK, lifetime is extended
Ref r2(GetInt()); // dangling, lifetime is not extended

S{1, 2, 3}; // OK, brace elision, same as S{1,2,{3}}
S(1, 2, 3); // error, no brace elision

// values without initializers take default values or value-initialized(T{})
S{1}; // {1, 2, 0}
S(1); // {1, 2, 0}

// make_unique works now
auto ps = std::make_unique<S>(1, 2, S::S2{3});

// arrays are also supported
int arr1[](1, 2, 3);
int 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; استنتاج می‌کند. روش استفاده از آن برای ایجاد یک کلاس ساده رشته زمان کامپایل چنین است:

template<std::size_t N>
struct fixed_string{
    constexpr fixed_string(const char (&s)[N+1]) {
        std::copy_n(s, N + 1, str);
    }
    constexpr const char* data() const {
        return str;
    }
    constexpr std::size_t size() const {
        return N;
    }

    char str[N+1];
};

template<std::size_t N>
fixed_string(const char (&)[N])->fixed_string<N-1>;

// user-defined literals are also supported
template<fixed_string S>
constexpr auto operator""_cts(){
    return S;
}

// N for `S` will be deduced
template<fixed_string S>
void f(){
    std::cout << S.data() << ", " << S.size() << '\n';
}

f<"abc">(); // abc, 3
constexpr auto s = "def"_cts;
f<s>();     // def, 3

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

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

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

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

template<auto T>    // placeholder for any non-type template parameter
struct X{};

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

X<5> x1;
X<'c'> x2;
X<1.2> x3;
// with the help of CTAD for aggregates
X<Arr{{1,2,3}}> x4; // X<Arr<int, 3>>
X<Arr{"hi"}> x5;    // X<Arr<char, 3>>

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

template<auto T>
struct S{};

union U{
    int a;
    int b;
};

enum class E{
    A = 0,
    B = 0
};

struct C{
    int x;
    bool operator==(const C&) const{    // never equal
        return false;
    }
};

constexpr C c1{1};
constexpr C c2{1};
assert(c1 != c2);                           // not equal using operator==()
assert(memcmp(&c1, &c2, sizeof(C)) == 0);   // but equal bitwise
// thus, equal at compile-time, operator==() is not used
static_assert(std::is_same_v<S<c1>, S<c2>>);

constexpr E e1{E::A};
constexpr E e2{E::B};
// equal bitwise, enum's identity isn't taken into account
assert(memcmp(&e1, &e2, sizeof(E)) == 0);
static_assert(std::is_same_v<S<e1>, S<e2>>); // thus, equal at compile-time

constexpr U u1{.a=1};
constexpr U u2{.b=1};
// equal bitwise but have different active members(a vs. b)
assert(memcmp(&u1, &u2, sizeof(U)) == 0);
// thus, not equal at compile-time
static_assert(!std::is_same_v<S<u1>, S<u2>>);

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

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

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

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

struct S{
    int a: 1;
    int b: 1;
    int c;
};

static auto [A,B,C] = S{};

void f(){
    [[maybe_unused]] thread_local auto [a,b,c] = S{};
    auto l = [=](){
        return a + b + c;
    };

    auto m = [&](){
        // error, can't capture bit-fields 'a' and 'b' by-reference
        // return a + b + c;
        return c;
    };
}

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

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

struct X : private std::shared_ptr<int>{
    std::string payload;
};

// due to new rules, this function is used instead of std::shared_ptr<int>::get
template<int N>
std::string& get(X& x) {
    if constexpr(N==0) return x.payload;
}

namespace std {
    template<>
    class tuple_size<X> 
        : public std::integral_constant<int, 1>
    {};
    
    template<>
    class tuple_element<0, X> {
    public:
        using type = std::string;
    };
}

void f(){
    X x;
    auto& [payload] = x;
}

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

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

struct A {
    friend void foo();
private:
    int i;
};

void foo() {
    A a;
    auto x = a.i;   // OK
    auto [y] = a;   // Ill-formed until C++20, now OK
}

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

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

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

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

class Obj{
    std::vector<int>& GetItems();
};

Obj GetObj();

// dangling reference, lifetime of Obj return by GetObj() is not extended
for(auto x : GetObj().GetCollection()){
    // ...
}

// OK
for(auto obj = GetObj(); auto item : obj.GetCollection()){
    // ...
}

// also can be used to maintain index
for(std::size_t i = 0; auto& v : collection){
    // use v...
    i++;
}

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

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

struct X : std::stringstream {
  // ...
};

std::istream_iterator<char> begin(X& x){
    return std::istream_iterator<char>(x);
}

std::istream_iterator<char> end(X& x){
    return std::istream_iterator<char>();
}

void f(){
    X x;
    // X has member with name `end` inherited from std::stringstream
    // but due to new rules free begin()/end() are used
    for (auto&& i : x) {
        // ...
    }
}

خصوصیت‌ها

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

[[likely]] و [[unlikely]]

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

int f(bool b){
    if(b) [[likely]] {
        return 12;
    }
    else{
        return 10;
    }
}

خصوصیت [[no_unique_address]]

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

struct Empty{};

template<typename T>
struct Cpp17Widget{
    int i;
    T t;
};

template<typename T>
struct Cpp20Widget{
    int i;
    [[no_unique_address]] T t;
};

static_assert(sizeof(Cpp17Widget<Empty>) > sizeof(int));
static_assert(sizeof(Cpp20Widget<Empty>) == sizeof(int));

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

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

// test whether it's supported
static_assert(__has_cpp_attribute(nodiscard) == 201907L);

[[nodiscard("Don't leave me alone")]]
int get();

void f(){
    get(); // warning: ignoring return value of function declared with 
           // 'nodiscard' attribute: Don't leave me alone
}

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

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

struct resource{
    // empty resource, no harm if discarded
    resource() = default;
    
    [[nodiscard("don't discard non-empty resource")]]
    resource(int fd);
};

void f(){
    resource{};     // OK
    resource{1};    // warning
}

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

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

char8_t

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

void HandleString(const char*){}
// distinct function name is required to handle UTF-8 in C++17
void HandleStringUTF8(const char*){}
// now it can be done using convenient overload
void HandleString(const char8_t*){}

void Cpp17(){
    HandleString("abc");        // char[4]
    HandleStringUTF8(u8"abc");  // C++17: char[4] but UTF-8, 
                                // C++20: error, type is char8_t[4]
}

void Cpp20(){
    HandleString("abc");    // char
    HandleString(u8"abc");  // char8_t
}

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

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

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

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

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

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

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

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

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

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

// until C++20:
struct S{
    int a : 1;
    int b : 1;
    S() : a{0}, b{1}{}
};

// since C++20:
struct S{
    int a : 1 {0},
    int b : 1 = 1;
};

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

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

template <class T>
T::R f();  // OK, return type of a function declaration at global scope

template <class T>
void f(T::R);   // Ill-formed (no diagnostic required), attempt to declare a
                // void variable template

template<typename T>
struct PtrTraits{
    using Ptr = void*;
};

template <class T>
struct S {
  using Ptr = PtrTraits<T>::Ptr;  // OK, in a defining-type-id
  T::R f(T::P p) {                // OK, class scope
    return static_cast<T::R>(p);  // OK, type-id of a static_cast
  }
  auto g() -> S<T*>::Ptr; // OK, trailing-return-type

  T::SubType t;
};

template <typename T>
void f() {
  void (*pf)(T::X); // Variable pf of type void* initialized with T::X
  void g(T::X);     // Error: T::X at block scope does not denote a type
                    // (attempt to declare a void variable)
}

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

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

// C++20
namespace A::B::inline C{
    void f(){}
}
// C++17
namespace A::B{
    inline namespace C{
        void f(){}
    }
}

استفاده از enum

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

namespace my_lib {
enum class color { red, green, blue };
enum COLOR {RED, GREEN, BLUE};
enum class side {left, right};
}

void f(my_lib::color c1, my_lib::COLOR c2){
    using enum my_lib::color;   // introduce scoped enum
    using enum my_lib::COLOR;   // introduce unscoped enum
    using my_lib::side::left;   // introduce single enumerator id

    // C++17
    if(c1 == my_lib::color::red){/*...*/}
    
    // C++20
    if(c1 == green){/*...*/}
    if(c2 == BLUE){/*...*/}

    auto r = my_lib::side::right;   // qualified id is required for `right`
    auto l = left;                  // but not for `left`
}

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

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

// before C++20
int p0[]{1, 2, 3};
int* p1 = new int[3]{1, 2, 3};  // explicit size is required

// since C++20
int* p2 = new int[]{1, 2, 3};
int* p3 = new int[]{};  // empty
char* p4 = new char[]{"hi"};
// works with parenthesized initialization of aggregates
int p5[](1, 2, 3);
int* p6 = new int[](1, 2, 3);

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

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

template<typename T>
using IntPair = std::pair<int, T>;

double d{};
IntPair<double> p0{1, d};   // C++17
IntPair p1{1, d};   // std::pair<int, double>
IntPair p2{1, p1};  // std::pair<int, std::pair<int, double>>

Constinit

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

struct S {
    constexpr S(int) {}
    ~S(){}; // non-trivial
};

constinit S s1{42};  // OK
constexpr S s2{42};  // error because destructor is not trivial

// tls_definitions.cpp
thread_local constinit int tls1{1};
thread_local int tls2{2};

// main.cpp
extern thread_local constinit int tls1;
extern thread_local int tls2;

int get_tls1() {
    return tls1;  // pure TLS access
}

int get_tls2() {
    return tls2;  // has implicit TLS initialization code
}

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

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

int i1 = -1;
// left-shift for signed negative integers(previously undefined behavior)
i1 <<= 1;    // -2

int i2 = INT_MAX;
// "unrepresentable" left-shift for signed integers(previously undefined behavior)
i2 <<= 1;   // -2

int i3 = -1;
// right shift for signed negative integers, performs sign-extension(previously 
// implementation-defined)
i3 >>= 1;   // -1
int i4 = 1;
i4 >>= 1;   // 0

// "unrepresentable" conversions to signed integers(previously implementation-defined)
int i5 = UINT_MAX;  // -1

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

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

#define LOG1(...)                   \
    __VA_OPT__(std::printf(__VA_ARGS);) \
    std::printf("\n");

LOG1();                      // std::printf("\n");
LOG1("number is %d", 12);    // std::printf("number is %d", 12); std::printf("\n");

#define LOG2(msg, ...) \
    std::printf("[" __FILE__ ":%d] " msg, __LINE__, __VA_ARGS__)
#define LOG3(msg, ...) \
    std::printf("[" __FILE__ ":%d] " msg, __LINE__ __VA_OPT__(,) __VA_ARGS__)

// OK, std::printf("[" "file.cpp" ":%d] " "%d errors.\n", 14, 0);
LOG2("%d errors\n", 0);

// Error, std::printf("[" "file.cpp" ":%d] " "No errors\n", 17, );
LOG2("No errors\n");

// OK, std::printf("[" "file.cpp" ":%d] " "No errors\n", 20);
LOG3("No errors\n");

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

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

struct S1{
    // ill-formed until C++20 because implicit constructor is noexcept(true)
    S1(S1&&)noexcept(false) = default; // can throw
};

struct S2{
    S2(S2&&) noexcept = default;
    // implicitly generated move constructor would be `noexcept(false)`
    // because of `s1`, now it's enforced to be `noexcept(true)`
    S1 s1;
};

static_assert(std::is_nothrow_move_constructible_v<S1> == false);
static_assert(std::is_nothrow_move_constructible_v<S2> == true);

struct X1{
    X1(X1&&) noexcept = default;
    std::map<int, int> m;   // `std::map(std::map&&)` can throw
};

struct X2{
    // same as implicitly generated, it's `noexcept(false)` because of `std::map`
    X2(X2&&) = default;
    std::map<int, int> m;   // `std::map(std::map&&)` can throw
};

std::vector<X1> v1;
std::vector<X2> v2;
// ... at some point, `push_back()` needs to reallocate storage

// efficiently uses `X1(X1&&)` to move the elements to a new storage,
// calls `std::terminate()` if it throws
v1.push_back(X1{});

// uses `X2(const X2&)`, thus, copies, not moves elements to a new storage
v2.push_back(X2{});

تخریب operator delete

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

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

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

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

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

template<typename T>
struct S{
    explicit(!std::is_convertible_v<T, int>) S(T){}
};

void f(){
    S<char> sc = 'x';           // OK
    S<std::string> ss1 = "x";   // Error, constructor is explicit
    S<std::string> ss2{"x"};    // OK
}

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

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

#ifdef __has_cpp_attribute  // check __has_cpp_attribute itself before using it
#   if __has_cpp_attribute(no_unique_address) >= 201803L
#       define CXX20_NO_UNIQUE_ADDR [[no_unique_address]]
#   endif
#endif

#ifndef CXX20_NO_UNIQUE_ADDR
#   define CXX20_NO_UNIQUE_ADDR
#endif

template<typename T>
class Widget{
    int x;
    CXX20_NO_UNIQUE_ADDR T obj;
};

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

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

void f(int (&&)[]){};
void f(int (&)[1]){};

void g() {
  int arr[1];

  f(arr);       // calls `f(int (&)[1])`
  f({1, 2});    // calls `f(int (&&)[])`
  int(&r)[] = arr;
}

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

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

std::unique_ptr<T> f0(std::unique_ptr<T> && ptr) {
    return ptr; // copied in C++17(thus, error), moved in C++20, OK
}

std::string f1(std::string && x) {
    return x;   // copied in C++17, moved in C++20
}

struct Widget{};

void f2(Widget w){
    throw w;    // copied in C++17, moved in C++20
}

struct From {
    From(Widget const &);
    From(Widget&&);
};

struct To {
    operator Widget() const &;
    operator Widget() &&;
};

From f3() {
    Widget w;
    return w;  // moved (no NRVO because of different types)
}

Widget f4() {
    To t;
    return t;// copied in C++17(conversions were not considered), moved in C++20
}

struct A{
    A(const Widget&);
    A(Widget&&);
};

struct B{
    B(Widget);
};

A f5() {
    Widget w;
    return w;  // moved
}

B f6() {
    Widget w;
    return w; // copied in C++17(because there's no B(Widget&&)), moved in C++20
}

struct Derived : Widget{};

std::shared_ptr<Widget> f7() {
    std::shared_ptr<Derived> result;
    return result;  // moved
}

Widget f8() {
    Derived result;
    // copied in C++17(because there's no Base(Derived)), moved in C++20
    return result;
}

تبدیل *T به bool محدود شده است

در نسخه جدید ++C تبدیل از انواع اشاره‌گر یا انواع «اشاره‌گر به عضو» به نوع bool محدود شده است و نمی‌تواند در مواردی که این تبدیل‌ها مجاز نیستد استفاده شود. nullptr در مواردی که با مقداردهی مستقیم استفاده شود مشکلی ندارد.

struct S{
    int i;
    bool b;
};

void f(){
    void* p;
    S s{1, p};          // error
    bool b1{p};         // error
    bool b2 = p;        // OK
    bool b3{nullptr};   // OK
    bool b4 = nullptr;  // error
    bool b5 = {nullptr};// error
    if(p){/*...*/}      // OK
}

منسوخ شدن برخی کاربردهای volatile

در C++20 برخی کاربردهای volatile در چارچوب‌های زیر منسوخ شده است.

  • عملگرهای داخلی پیشوندی/پسوندی افزایشی/ کاهشی روی متغیرهای احراز شده فرّار
  • کاربرد نتیجه یک انتساب به شیء احراز شده فرّار
  • انتساب‌های ترکیبی داخلی به شکل E1 op= E2 در حالتی که E1 احراز شده فرار باشد.
  • نوع بازگشتی/پارامتر احراز شده فرار
  • اعلان‌های اتصال ساخت‌یافته احراز شده فرّار

توجه کنید که در حملات فوق منظور از «احراز شده فرّار» (volatile-qualified) احراز سطح بالا است و نه هر نوع volatile در یک نوع. گاهی اوقات در مواردی مانند volatile int* px می‌بینیم که در عمل یک اشاره‌گر به in فرار است و از این رو احراز شده فرار نیست.

volatile int x{};
x++;            // deprecated
int y = x = 1;  // deprecated
x = 1;          // OK
y = x;          // OK
x += 2;         // deprecated

volatile int            //deprecated
    f(volatile int);    //deprecated

منسوخ شدن عملگر کاما در زیرنویس‌ها

عملگر کاما درون زیرنویس‌ها منسوخ شده است تا عملگر زیرنویس چند بعدی (متغیرپذیر) در آینده مجاز باشد. رویکرد کنونی به این موضوع این است که path_type سفارشی با path_type::operator, () و operator[](path_type) اورلود داشته باشیم. []operator متغیرپذیر نیاز به چنین ترفندهای پیچیده را حذف می‌کند.

// current approach
struct SPath{
    SPath(int);
    SPath operator,(const SPath&);  // store path somehow
};

struct S1{
    int operator[](SPath); // use path
};

S1 s1;
auto x1 = s1[1,2,3];    // deprecated
auto x2 = s1[(1,2,3)];  // OK

// future approach
struct S2{
    int operator[](int, int, int);
    // or, as a variadic template
    template<typename... IndexType>
    int operator[](IndexType...);
};

S2 s2;
auto x3 = s2[1,2,3];

اصلاحیه‌ها

در این بخش برخی اصلاحیه‌های جزئی مطرح شده در نسخه 20 زبان برنامه‌نویسی سی پلاس پلاس را بررسی می‌کنیم. برخی از این موارد مدت‌ها است که از سوی کامپایلر پیاده‌سازی شده‌اند اما در استاندارد بازتاب نیافته‌اند. شاید این موارد را در عمل مشاهده نکنید.

سازنده‌های لیست مقداردهی در استنتاج آرگومان تمپلیت کلاس

// C++17
std::tuple t{std::tuple{1, 2}};// std::tuple<int, int>
std::vector v{std::vector{1,2,3}};// std::vector<std::vector<int>>

در این مثال، دو مقداردهی با ترکیب یکسان موجب ایجاد دو نوع استنتاج CATD متفاوت شده‌اند. دلیل این امر آن است که std::vector دارای سازنده std::initializer_list است و آن را ترجیح می‌دهد . در حالی که std::tuple چنین چیزی ندارد و سازنده copy را ترجیح می‌دهد.

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

// C++20
std::tuple t{std::tuple{1, 2}};     // std::tuple<int, int>
std::vector v{std::vector{1,2,3}};  // std::vector<int>

// this example is from "C++17" book by N. Josuttis, section 9.1.1
// now it has consistent behavior across compilers
template<typename... Args>
auto make_vector(const Args&... elems)
{
    return std::vector{elems...};
}

auto v2 = make_vector(std::vector{1,2,3});  // std::vector<int>

اشاره‌گرهای احراز شده const& به اعضا

مشکل سابقاً این بود که استفاده از.* با rvalue دارای اشاره‌گر احراز شده ارجاع به تابع عضو مجاز نبود. اینک مجاز است.

struct S {
    void f() const& {}
};

S{}.f();        // OK
(S{}.*&S::f)(); // could be an error on some old compilers

ساده‌سازی دریافت لامبدای ضمنی

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

struct S{
    int x{1};
    int y{[&]{ return x + 1; }()};  // OK, captures 'this'
};

موجودیت‌ها به طور ضمنی دریافت می‌شوند، هر چند درون گزاره‌های حذف شده و typeid باشند:

template<bool B>
void f1() {
    std::unique_ptr<int> p;
    [=]() {
        if constexpr (B) {
            (void)p;        // always captures p
        }
    }();
}
f1<false>();    // error, can't capture unique_ptr by-value

void f2() {
    std::unique_ptr<int> p;
    [=]() {
        typeid(p);  // error, can't capture unique_ptr by-value
    }();
}

void f3() {
    std::unique_ptr<int> p;
    [=]() {
        sizeof(p);  // OK, unevaluated operand
    }();
}

عدم تطبیق Const با سازنده کپی پیش‌فرض

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

struct NonConstCopyable{

NonConstCopyable() = default;

NonConstCopyable(NonConstCopyable&){}// takes by non-const reference

NonConstCopyable(NonConstCopyable&&){}

};

// std::tuple(const std::tuple& other) = default;// takes by const reference

void f(){

std::tuple<NonConstCopyable> t; // error in C++17, OK in C++20

auto t2 = t;// always an error

auto t3 = std::move(t);// OK, move-ctor is used

}

بررسی دسترسی روی specialization-ها

اینک امکان استفاده از نوع protected/private به عنوان آرگومان تمپلیت برای specialization جزئی وجود دارد و به این ترتیب امکان specialization صریح و وهله‌سازی صریح وجود دارد.

template<typename T>
void f(){}

template<typename T>
struct Trait{};

class C{
    class Impl; // private
};

template<>
struct Trait<C::Impl>{};    // OK

template struct Trait<C::Impl>; // OK

class C2{
    template<typename T>
    struct Impl;    // private
};

template<typename T>
struct Trait<C2::Impl<T>>;   // OK

ADL و تمپلیت‌های تابع ناپیدا

Id احراز نشده که در ادامه‌اش یک < آمده باشد و بررسی نام در مورد آن چیزی پیدا نمی‌کند یا تابعی پیدا کند به عنوان یک نام تمپلیت برخورد می‌شود تا به طور بالقوه موجب اجرای بررسی وابستگی آرگومان شود.

int h;
void g();

namespace N {
	struct A {};
	template<class T> int f(T);
	template<class T> int g(T);
	template<class T> int h(T);
}

// OK: lookup of `f` finds nothing, `f` treated as a template name
auto a = f<N::A>(N::A{});
// OK: lookup of `g` finds a function, `g` treated as a template name
auto b = g<N::A>(N::A{});
// error: `h` is a variable, not a template function
auto c = h<N::A>(N::A{};
// OK, `N::h` is qualified-id
auto d = N::h<N::A>(N::A{});

در موارد نادری اگر ()<operator برای تابع‌ها موجود باشد این وضعیت موجب از کار افتادن کد موجود می‌شود،، اما این وضعیت از سوی کمیته به صورت آسیب‌شناسی در نظر گرفته شده است.

struct A {};
bool operator <(void (*fp)(), A);
void f(){}
int main() {
    A a;
    f < a;      // OK until C++20, now error
    (f) < a;    // OK
}

تعیین زمان نیاز به تعاریف تابع constexpr برای ارزیابی ثابت

این اصلاحیه زمان نیاز به وهله‌سازی از تابع‌های constexpr را مشخص می‌سازد. این قواعد کاملاً پیچیده هستند اما در اغلب موارد کارها مطابق انتظار پیش می‌رود. به جای کپی کردن و چسباندن قواعد در این بخش از مقاله چند مثال ارائه شده تا موضوع روشن‌تر شود.

struct duration {
    constexpr duration() {}
    constexpr operator int() const { return 0; }
};

// duration d = duration(); // #1
int n = sizeof(short{duration(duration())});    // always OK since C++20

به خاطر داشته باشید که تابع‌های عضو خاص تنها زمانی تعریف می‌شوند که مورد استفاده قرار گیرند. با اصطلاحات C++17، سازنده جابجایی در اینجا استفاده نمی‌شود و لذا تعریف نشده و از این جهت برنامه خوش-تعریف نیست. اما اگر خط شماره 1 را از کامنت خارج کنیم، سازنده move مورد استفاده قرار می‌گیرد و لذا تعریف شده است و برنامه حالت مناسبی می‌یابد. این کار معنای خاصی ندارد و از این رو قواعد برای لحاظ کردن این مشکل تغییر یافته‌اند.

مثال دیگر به صورت زیر است:

template<typename T> constexpr int f() { return T::value; }

template<bool B, typename T> void g(decltype(B ? f<T>() : 0));
template<bool B, typename T> void g(...);

template<bool B, typename T> void h(decltype(int{B ? f<T>() : 0}));
template<bool B, typename T> void h(...);

void x() {
    g<false, int>(0); // OK
    h<false, int>(0); // error
}

در مثال فوق، تابع تمپلیت constexpr را داریم که به طور بالقوه با نوع int وهله‌سازی شده و باید منجر به بروز خطا شود، زیرا int::value نادرست است. در ادامه دو تابع هستند که از B? f<int>(): 0 استفاده می‌کنند و B همواره false است و از این رو ()<f<int هرگز لازم نخواهد بود. سؤال این است که آیا <f<int باید در اینجا وهله‌سازی شود؟

قواعد جدید آنچه برای ارزیابی ثابت نیاز است را مشخص کرده‌اند. متغیرهای تمپلیت یا تابع‌ها در چنین عبارت‌هایی باید همواره وهله‌سازی شوند هر چند نیازی به ارزیابی عیارت نباشد. یکی از این موارد لیست مقداردهی آکولادی است، از این رو وهله‌سازی در عبارت <int{B? f<T>(): 0} f<T همواره موجب بروز خطا خواهد شد.

ایجاد ضمنی اشیا برای دستکاری سطح پایین شیء

در C++17 یک شیء می‌تواند با تعریف با عبارت new یا با تغییر دادن عضو فعال یک union ایجاد شود. اینک مثال زیر را در نظر بگیرید:

struct X { int a, b; };
X *make_x() {
    X* p = (X*)malloc(sizeof(struct X));
    p->a = 1;   // UB in C++17, OK in C++20
    return p;
}

با این ک این وضعیت طبیعی به نظر می‌رسد، اما این کد در C++17 رفتار تعریف نشده‌ای دارد زیرا x بر اساس قواعد زبان ایجاد نشده و نوشتن در عضو یک موجودیت غیر موجود UB است. قواعد چنین وضعیتی با تعیین نوع‌هایی که می‌توانند به طور ضمنی ایجاد شوند و عملیاتی که می‌توانند این اشیا را به صورت ضمنی ایجاد کنند روشن‌سازی شده است. انواعی که می‌توانند به طور ضمنی ایجاد شوند به شرح زیر هستند:

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

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

  • عملیاتی که با طول عمر یک ارائه از نوع char, unsigned char, std::byte آغاز می‌شوند.
  • عملگر new و عملگر []new
  • std::allocator<T>::allocate(std::size_t n)
  • تابع‌های تخصیص کتابخانه C به صورت aligned_alloc, calloc, malloc, و realloc
  • memcpy و memmove
  • std::bit_cast

همچنین قاعده برای شبه تخریب‌گر (تخریبگر برای انواع داخلی) تغییر یافته است. تا قبل از C++20 این تخریبگر هیچ تأثیری نداشت اما اکنون طول عمر شیء را پایان می‌بخشد.

int f(){
    using T = int;
    T n{1};
    n.~T();     // no effect in C++17, ends n's lifetime in C++20
    return n;   // OK in C++17, UB in C++20, n is dead now
}

سخن پایانی

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

بر اساس رای ۰ نفر
آیا این مطلب برای شما مفید بود؟
شما قبلا رای داده‌اید!
اگر بازخوردی درباره این مطلب دارید یا پرسشی دارید که بدون پاسخ مانده است، آن را از طریق بخش نظرات مطرح کنید.
منابع:
oleksandrkvl

نظر شما چیست؟

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *