C++20 の新しい属性

C++20 では、[[nodiscard("reason")]]、[[likely]]、[[unlikely]]、[[no_unique_address]] などの新しく改善された属性を取得しました。特に、[[nodiscard("reason")]] を使用すると、インターフェイスの意図をより明確に表現できます。

属性を使用すると、コードの意図を宣言的に表現できます。

新しい属性

この記事を書いているうちに、私は [[nodiscard("reason")]] の大ファンになりました。したがって、お気に入りから始めたいと思います。

[[nodiscard("理由")]]

[[nodiscard]] は C++17 から既にあります。 C++20 では、属性にメッセージを追加する可能性が追加されました。残念ながら、私はここ数年 [[nodiscard]] を無視していました。さっそく提示させてください。次のプログラムがあると想像してください。

// withoutNodiscard.cpp

#include <utility>

struct MyType {

 MyType(int, bool) {}

};

template <typename T, typename ... Args>
T* create(Args&& ... args){
 return new T(std::forward<Args>(args)...);

enum class ErrorCode {
 Okay,
 Warning,
 Critical,
 Fatal
};

ErrorCode errorProneFunction() { return ErrorCode::Fatal; }

int main() {

 int* val = create<int>(5);
 delete val;

 create<int>(5); // (1)

 errorProneFunction(); // (2)
 
 MyType(5, true); // (3)

} 

完全な転送とパラメーター パックのおかげで、ファクトリ関数 create はすべてのコンストラクターを呼び出して、ヒープ割り当てオブジェクトを返すことができます。

プログラムには多くの問題があります。まず、行 (1) でメモリ リークが発生しています。これは、作成されたヒープ上の int が決して破棄されないためです。次に、関数 errorPronceFunction (2) のエラー コードがチェックされません。最後に、コンストラクター呼び出し MyType(5, true) は、作成されてすぐに破棄される一時を作成します。これは少なくともリソースの無駄です。今、[[nodiscard]] の出番です。

[[nodiscard]] は、関数宣言、列挙宣言、またはクラス宣言で使用できます。 nodiscard として宣言された関数からの戻り値を破棄すると、コンパイラは警告を発行する必要があります。 [[nodiscard]] として宣言された列挙型またはクラスをコピーして返す関数にも同じことが当てはまります。 void へのキャストは警告を出すべきではありません。

これが何を意味するのか見てみましょう。次の例では、属性 [[nodiscard]] の C++17 構文を使用しています。

// nodiscard.cpp

#include <utility>

struct MyType {

 MyType(int, bool) {}

};

template <typename T, typename ... Args>
[[nodiscard]]
T* create(Args&& ... args){
 return new T(std::forward<Args>(args)...);
}

enum class [[nodiscard]] ErrorCode {
 Okay,
 Warning,
 Critical,
 Fatal
};

ErrorCode errorProneFunction() { return ErrorCode::Fatal; }

int main() {

 int* val = create<int>(5);
 delete val;

 create<int>(5); // (1)

 errorProneFunction(); // (2)
 
 MyType(5, true); // (3)

}

ファクトリ関数 create と enum ErrorCode は [[nodiscard]] として宣言されています。したがって、呼び出し (1) と (2) は警告を生成します。

ずっと良くなりましたが、プログラムにはまだいくつかの問題があります。 [[nodiscard]] は、何も返さないコンストラクタなどの関数には使用できません。したがって、一時的な MyType(5, true) は引き続き警告なしで作成されます。次に、エラー メッセージが一般的すぎます。関数のユーザーとして、結果の破棄が問題になる理由を知りたいです。

どちらの問題も C++20 で解決できます。コンストラクターは [[nodiscard]] として宣言でき、警告には追加情報が含まれる場合があります。

// nodiscardString.cpp

#include <utility>

struct MyType {

 [[nodiscard("Implicit destroying of temporary MyInt.")]] MyType(int, bool) {}

};

template <typename T, typename ... Args>
[[nodiscard("You have a memory leak.")]]
T* create(Args&& ... args){
 return new T(std::forward<Args>(args)...);
}

enum class [[nodiscard("Don't ignore the error code.")]] ErrorCode {
 Okay,
 Warning,
 Critical,
 Fatal
};

ErrorCode errorProneFunction() { return ErrorCode::Fatal; }

int main() {

 int* val = create<int>(5);
 delete val;

 create<int>(5); // (1)

 errorProneFunction(); // (2)
 
 MyType(5, true); // (3)

}

これで、関数のユーザーは特定のメッセージを受け取ります。 Microsoft コンパイラの出力は次のとおりです。

ところで、C++ の既存の関数の多くは、[[nodiscard]] 属性の恩恵を受けることができます。たとえば、std::asnyc の戻り値を使用しない場合、非同期を意味する std::async 呼び出しは暗黙的に同期になります。別のスレッドで実行する必要があるものは、ブロッキング関数呼び出しとして動作します。 std::async の直観に反する動作について詳しくは、私のブログ「The Special Futures」をご覧ください。

cppreference.com で [[nodiscard]] 構文を調べているときに、std::async のオーバーロードが C++20 で変更されていることに気付きました。ここに 1 つがあります:

template< class Function, class... Args>
[[nodiscard]]
std::future<std::invoke_result_t<std::decay_t<Function>,
 std::decay_t<Args>...>>
 async( Function&& f, Args&&... args );

promise std::async の return-type としての std::future は [[nodiscard]] として宣言されます。

次の 2 つの属性 [[可能性が高い]] と [[可能性が低い]] は、最適化に関するものです。

[[可能性が高い]] と [[可能性が低い]]

可能性が高い属性と可能性が低い属性の提案 P0479R5 は、私が知っている最短の提案です。アイデアを提供するために、これは提案に対する興味深いメモです。 "可能性の使用 属性は、それを含む実行パスが、ステートメントまたはラベルにそのような属性を含まない代替実行パスよりも任意に可能性が高い場合に、実装が最適化できるようにすることを目的としています。 可能性が低いの使用 属性は、それを含む実行パスが、ステートメントまたはラベルにそのような属性を含まない代替実行パスよりも任意に可能性が低い場合に、実装が最適化できるようにすることを目的としています。実行パスには、そのラベルへのジャンプが含まれている場合にのみ、そのラベルが含まれます。これらの属性のいずれかを過度に使用すると、パフォーマンスが低下する可能性があります ."

簡潔に言うと、どちらの属性もオプティマイザーにヒントを与えることを可能にし、どちらの実行パスが多かれ少なかれ可能性が高いかを示します。

for(size_t i=0; i < v.size(); ++i){
 if (v[i] < 0) [[likely]] sum -= sqrt(-v[i]);
 else sum += sqrt(v[i]);
}

最適化の話は、新しい属性 [[no_unique_address]] で続きます。今回の最適化はスペースに対処します。

[[no_unique_address]]

[[no_unique_address]] は、クラスのこのデータ メンバーが、そのクラスの他のすべての非静的データ メンバーと異なるアドレスを持つ必要がないことを表します。したがって、メンバーに空の型がある場合、コンパイラはメモリを占有しないように最適化できます。

次のプログラムは、新しい属性の使用例です。

// uniqueAddress.cpp

#include <iostream>
 
struct Empty {}; 
 
struct NoUniqueAddress {
 int d{};
 Empty e{};
};
 
struct UniqueAddress {
 int d{};
 [[no_unique_address]] Empty e{}; // (1)
};
 
int main() {

 std::cout << std::endl;
 
 std::cout << std::boolalpha;

 std::cout << "sizeof(int) == sizeof(NoUniqueAddress): " // (2)
 << (sizeof(int) == sizeof(NoUniqueAddress)) << std::endl;
 
 std::cout << "sizeof(int) == sizeof(UniqueAddress): " // (3)
 << (sizeof(int) == sizeof(UniqueAddress)) << std::endl;
 
 std::cout << std::endl;
 
 NoUniqueAddress NoUnique;
 
 std::cout << "&NoUnique.d: " << &NoUnique.d << std::endl; // (4)
 std::cout << "&NoUnique.e: " << &NoUnique.e << std::endl; // (4)
 
 std::cout << std::endl;
 
 UniqueAddress unique;
 
 std::cout << "&unique.d: " << &unique.d << std::endl; // (5)
 std::cout << "&unique.e: " << &unique.e << std::endl; // (5)
 
 std::cout << std::endl;

}

クラス NoUniqueAddress には別のサイズの int (2) がありますが、クラス UniqueAddress (3) にはありません。 NoUniqueAddress (4) のメンバー d と e は異なるアドレスを持ちますが、クラス UniqueAddress (5) のメンバーではありません。

次は?

volatile 修飾子は、C++ の最も暗いコーナーの 1 つです。その結果、C++20 ではほとんどの volatile が非推奨になりました。