文書の一覧
www.open-std.org
提案文書で採択されたものはありません。全部で29本あります。
std::atomic
に対して、指定した値と現在の値の大小関係によって値を書き換えるmaximum/minimum操作であるfetch_max()/fetch_min()
を追加する提案。
アトミックな数値演算は既に標準化されていますがmaximum/minimum操作はそうではなく、他フレームワークやハードウェアには既に実装があり、いくつかのマルチスレッドアプリケーションで有用であるため追加しようというものです。
#include <atomic>
std::atomic<int> a = 10;
int r1 = a.fetch_max(20);
int r2 = a.fetch_min(5);
これらの操作はread-modify-writeです。すなわち、現在の値と指定された値の大小関係に関わらず、値は常に更新されます。
std::atomic<int> a = 10;
int r = a.fetch_max(5);
int v = a.load();
int max = std::max(v, 5);
a.store(max);
int r = v;
この提案では今の所、std::atomic<T>
の整数型とポインタ型の特殊化に対してだけfetch_max()/fetch_min()
メンバ関数を追加しています。「P0020 : Floating Point Atomic」が採択されれば浮動小数点型の特殊化についても追加すると書かれていて、これはC++20に対して既に採択されているので、次のリビジョンくらいで浮動小数点型のstd::atomic<T>
特殊化についても同様のものが追加されるかもしれません。
また、他のatomic
操作に準ずる形で非メンバ関数版も用意されています。ただし、これらも整数型とポインタ型でのみ利用可能です。
namespace std {
template<class T>
T atomic_fetch_max(atomic<T>*, typename atomic<T>::value_type) noexcept;
template<class T>
T atomic_fetch_max_explicit(atomic<T>*, typename atomic<T>::value_type, memory_order) noexcept;
}
型T
が別の型U
へ縮小変換(narrowing conversion)によって変換可能かを調べるメタ関数is_narrowing_convertible<T, U>
を追加する提案。
前回の記事を参照
onihusube.hatenablog.com
このリビジョンでの主な変更は、機能テストマクロが追加された事と、配列を用いた実装がvoid
や参照型、配列型など一部の型で機能しない事が明記された事です。
std::string, std::string_view
に、指定された文字列が含まれているかを調べるcontains()
メンバ関数を追加する提案。
同じことは既にあるfind()
を使えばできますが、find()
関数を使用する方法には少し問題があります。
if (str.find(substr) != std::string::npos)
std::cout << "found!\n";
- 含まれているかを調べているのに
!=
を使用する(書きづらい)
- 調べているのは文字の位置なのか、含まれているかどうかなのか、含まれていないかどうかなのか、一見して分かりづらい(読みづらい)
対して、contains()
というメンバ関数は意図が明確で書くときも読むときもこれらの問題は起こらず、初学者に対しても教えやすく使いやすいものです。また、他の言語の文字列型および、標準外のライブラリには既に対応する関数の実装があるため、標準ライブラリにも追加しよう、と言うのが要旨です。
if (str.contains(substr))
std::cout << "found!\n";
オーバーロードはstarts_with/ends_with
を参考に以下の3つが提供されます。
constexpr bool contains(basic_string_view x) const noexcept;
constexpr bool contains(charT x) const noexcept;
constexpr bool contains(const charT* x) const;
std::numeric_limits
に代わる新たな数値特性(numeric traits)取得方法を導入する提案。
例えば数値型の最大値や最小値等、数値型の満たしている各種特性を取得するのに現在はstd::numeric_limits
が用意されています。これは少なくとも<type_traits>
ヘッダにあるような型特性が見出されるよりも以前から存在しており、その設計は古くなっています。
ユーザー定義型に対する特殊化を追加する場合、ジェネリックな利用のために本来必要のない数値特性についてもそれっぽい値を返すように実装する必要があります。あるいは、ある数値特性を提供しているのかどうかを知る方法が提供されていません。
このことは、新たな数値特性を追加した場合には既存のユーザー定義型に対する特殊化を破壊する事を意味しており、そのためにstd::numeric_limits
は拡張可能ではなくなっています。
そこで、std::numeric_limits
にある各数値特性関数をそれぞれ個別のクラステンプレートと対応する変数テンプレートのペアに分解します。また同時に、一部の数値特性の名前と内容を調整します。
template <class T>
struct finite_max;
template <class T>
struct finite_min;
template <class T>
inline constexpr auto finite_max_v = finite_max<T>::value;
template <class T>
inline constexpr auto finite_min_v = finite_min<T>::value;
ある型について任意の数値特性が定義されているかを調べるものも提供されます。
template <template <class> class Trait, class T>
inline constexpr bool value_exists;
template <template <class> class Trait, class T, class R = T>
inline constexpr R value_or(R def = R()) noexcept;
これは例えば、次のように実装されます
template <template <class> class Trait, class T>
constexpr bool value_exists = requires { Trait<T>::value; };
template <template <class> class Trait, class T, class R = T>
constexpr R value_or( R def = R() ) noexcept {
if constexpr(value_exists<Trait, T>s)
return Trait<T>::value;
else
return def;
}
これらのものは<numbers>
ヘッダとstd::numbers
名前空間に追加されます。
このように、数値型に対する数値特性が個別に分かれていることによって新しい数値特性を追加する際に既存のユーザー定義特殊化を壊してしまう事もありません。ユーザーが特殊化を追加する際も必要な数値特性についてだけ特殊化を行えばよくなります。
Networking TS(簡単に言えば、ソケット通信ライブラリ)に対して、TLS/DTLSのサポートをデフォルトにする提案。
今日、ネットワークに接続すると言うことは悪意を持った攻撃に曝されることを意味します。それに対処するために、インターネットにおける通信はHTTPS(TLS)等を用いてセキュアにする事がデフォルトとなりつつあります。特に、WEBサーバの中にはHTTPを拒否しHTTPSでしか通信をしないものも増えてきています。
C++のネットワークライブラリがそれらの現代のWEBシステムと対話するために、また、ネットワークセキュリティの知識のない開発者がそれを意識せずとも一定のセキュリティを確保する事ができるように、そして、C++のネットワークライブラリを用いたプログラムが将来的にもセキュアなインターネットと連携していくために、Networking TSにおいてTLS/DTLSをデフォルトで使用するようにする、と言う提案です。
セキュリテイを確保するために追加のややこしい設定が必要になったりコードとその理解が複雑になることはセキュアなプログラムを書くことを妨げ、安全でない通信の利用を促進しかねないため、この提案では現在のNetworking TSのAPIを変更し、WEBアクセスなども意識した使いやすいAPIセットを提案しています。
コルーチンとstd::lazy<T>
を用いたHTTPSクライアントのサンプル
#include <iostream>
#include <net>
std::lazy<void> run()
{
net::workqueue queue(net::workqueue::main_queue());
net::endpoint::host host("www.apple.com", 80);
net::connection connection(host, net::parameters::tls(), queue);
connection.start();
std::cout << "Sending request" << std::endl;
net::message message(net::buffer("GET / HTTP/1.1\r\nHost: www.apple.com\r\n\r\n"));
auto sendResult = co_await connection.send(message);
if (!sendResult) {
std::cerr << "failed to send request" << std::endl;
co_return;
}
std::cout << "Sent request, waiting for response" << std::endl;
auto message = co_await connection.receive();
if (!message) {
std::cerr << "failed to receive response" << std::endl;
co_return;
}
std::cout << "Received response" << std::endl;
message->data().get([](const uint8_t *bytes, std::size_t size) {
std::cout << std::string(reinterpret_cast<const char *>(bytes), size);
});
std::cout << std::endl;
co_return;
}
int main(int, char**)
{
auto lazy = run();
net::workqueue::main();
}
この提案は将来的にQUIC等のプロトコルをサポートするための下準備も兼ねています。
Executorライブラリにいくつかの汎用非同期アルゴリズムを追加する提案。
現在のExecutor提案に含まれている非同期アルゴリズムはバルク処理のためのbulk_execute
だけで、Executorを実用的にするためにもう少し多くの汎用非同期アルゴリズムを追加しよう、と言う提案です。
また、今後さらに多くの汎用非同期アルゴリズムを追加していくにあたって、より洗練された設計や文言を選択するために、個別に議論可能な(相互依存していない)最小のアルゴリズムのセットから提案を始めています。
追加されるものは以下のものです(引数のs
は何か処理を示すsenderオブジェクト)。なおこれらのものは全てカスタマイゼーションポイントオブジェクトです。
just(v...)
just_on(scheduler, v, ...)
on
の効果とセットになっているjust()
scheduler
の実行コンテキスト上でjust(v...)
するsenderを返す
on(s, scheduler)
scheduler
の実行コンテキスト上で実行されるs
から、結果値かエラーを伝播するsenderを返す
sync_wait(s)
s
を実行し、処理の結果を返すか、処理中の例外が送出されるか、どちらかによって完了するのを待機する
- 戻り値は
s
の結果、s
の実行に際する例外を送出する
when_all(s...)
- 全ての
s...
の処理が完了するとその処理も完了するsenderを返す。全ての結果値が伝播される。
transform(s, f)
s
の結果にf()
を適用するか、エラーかキャンセルを伝播するsenderを返す。
let_value(s, f)
s
の結果値が、別の非同期処理f
の実行中利用可能となる非同期スコープを作成する
s
のエラーやキャンセルは変更されずに伝播される
let_error(s, f)
s
のエラー値が、別の非同期処理f
の実行中利用可能となる非同期スコープを作成する
s
の結果値やキャンセルは変更されずに伝播される
ensure_started(s)
- 即座に
s
を実行コンテキストへ投入し、その他のコードと並行に実行されている可能性のあるsenderを返す
提案文書より、簡単なサンプル。
auto just_sender = just(3);
auto transform_sender = transform(
std::move(just_sender),
[](int a){return a+0.5f;}
);
float result = sync_wait(std::move(transform_sender));
float f = sync_wait(
just(3) | transform([](int a){return a+0.5f;})
);
複数の処理(sender)を受けてそれらを直列化するwhen_all
のサンプル。
auto just_sender = just(std::vector<int>{3, 4, 5}, 10);
auto just_int_sender = just(3);
auto just_float_sender = just(20.0f);
auto when_all_sender = when_all(
std::move(just_sender),
std::move(just_int_sender),
std::move(just_float_sender)
);
auto transform_sender = transform(
std::move(when_all_sender),
[](std::vector<int> vec, int , float ) {
return vec;
}
);
vector<int> result = sync_wait(std::move(transform_sender));
vector<int> result_vec = sync_wait(
when_all(
just(std::vector<int>{3, 4, 5}, 10),
just(3),
just(20.0f)
) |
transform([](vector<int> vec, int , float ){return vec;})
);
Executorにおける処理の前方進行と非同期処理グラフのモデルに関する提案。
Executorライブラリと非同期アルゴリズムによってワークチェーンを構成し実行する際にその実行リソース(実行コンテキスト、scheduler)がどのように伝播するのかを明確に定義するものです。
新しくscheduler_provider
コンセプトとget_scheduler
CPOの2つを追加します。scheduler_provider
コンセプトは(receiverに対して)get_scheduler()
によってschedulerを取得可能であることを求めます。senderはconnect()
されたscheduler_provider
(なreceiver)からその実行コンテキストであるschedulerを取得する事で非同期タスクの下流から上流、あるいは上流から下流に向かってschedulerを伝播させることが可能になります。
複数の処理をチェーンするとき、個々の処理を示すsenderオブジェクトもその順番通りに内部で紐づいていき、最後にそれらの処理全体を示す1つのsenderオブジェクトが得られます。そこにその処理のコールバックとなるreceiverを接続(connect()
)して非同期処理の完了(成功、失敗、キャンセル)を待機できるようなoperation stateオブジェクトが得られます。そして、最後にoperation stateオブジェクトをstart()
などで明示的に開始します。
senderとreceiverのconnect()
の際は、渡されたreceiverオブジェクトはチェーンされたsender列の最後から先頭へ伝播していきます(実装によるかもしれません)。すなわち、チェーンされた処理を示す一連のsenderオブジェクトは全て同じ一つのreceiverオブジェクトを受け取ることになります。
sender auto begin = then(
std::execution::schedule( pool ),
[]{ return 1; }
);
sender auto task = begin | then([](auto n){ return n + 1;})
| then([](auto n){ return n * 2;})
| then([](auto n){ return n * n});
receiver auto rec = ;
operation_state auto state = std::execution::connect(task, rec);
std::execution::start(state);
この例では、最初のsenderに登録されたscheduler(どこかのスレッドプールとしている)が処理の上流から下流へ伝播するはずです。ただ、この例のように最初のsenderにいつも実行コンテキストが指定されるとは限りませんし、チェーンの途中でon()
などによってschedulerを変更することができます。また、非同期アルゴリズムの種類によってはどのschedulerで実行するべきか不明な場合もあります。
そのような時、その一連のsender全体に渡っているreceiverオブジェクトを介してあるsenderから別のsenderへschedulerをやり取りすることができると、適切なschedulerを選択できるかもしれません。
そのためにget_scheduler()
を追加し、それを用いればscheduler_provider
(なreceiver)からschedulerをチェーン上の任意の場所から任意の場所へ伝達できるようになります。もちろん、どのように伝達するのかはsenderの実装によることになります。
sender auto pool_sender = then(
std::execution::schedule( pool ),
[]{ return 1; }
);
sender auto task = when_all(
pool_sender,
just(1.0),
just("executor")
) | then([](int, double, const char*) { return true; });
operation_state auto state = std::execution::connect(task, rec);
コンパイル時に確保したメモリを実行時にも安全に参照するための要件と、そのためのより深いconst
性を指定するpropconst
の提案。
C++20からはconstexpr
な動的メモリ確保が可能になっていますが、Non-transientなメモリ確保(コンパイル時に確保したメモリを実行時にも参照すること)は許可されませんでした。
constexpr void f(std::initilizer_list<int> il) {
std::vector<int> vec = il;
}
int main() {
constexpr std::vector<int> vec = {1, 2, 3, 4, 5};
}
Non-transientなメモリ確保が許可されていた以前の仕様の下では、クラス内部で確保されるメモリで条件を満たした場合にコンパイル時に解放されなかったメモリは実行時に静的ストレージに昇格されて参照可能でした。その際は通常のconstexpr
変数と同様に実行時const
変数になります。その条件とは以下のようなものでした。
T
は非トリビアルconstexprデストラクタを持つ
- そのデストラクタはコンパイル時実行可能
- そのデストラクタ内で、
T
の初期化時に確保されたメモリ領域(Non-transient allocation)を解放する
すなわち、そのクラスのconstexpr
デストラクタによってコンパイル時に確保されたメモリがコンパイル時に解放可能であることです。これはコンパイラによるテスト要件であって、実際に解放が行われるわけではありません。
そして、その仕様の下では次のような問題が発生します。
constexpr std::unique_ptr<std::unique_ptr<int>> uui
= std::make_unique<std::unique_ptr<int>>(std::make_unique<int>());
int main() {
std::unique_ptr<int>& ui = *uui;
ui.reset();
}
このように、デストラクタを実行時に呼び出せてしまいますが、前述のテスト要件だけではこれを検出し防ぐことはできません。そのため、Non-transientなメモリ確保は最終的にリジェクトされました。
これが何故起こるかというと、std::unique_ptr
はdeep constな型ではないからです。すなわち、外側のstd::unique_ptr
のconst
性が内部のstd::unique_ptr
まで伝播していません。
そこで、以前のコンパイラによるデストラクタのテスト要件に次の条件を加えます。
- デストラクタ呼び出し中に現れる全ての(メンバ)変数は
const
であり、かつ
- そのデストラクタは実行時に破棄されうるオブジェクトに対して呼び出されていない
これによって、上記のstd::unique_ptr<std::unique_ptr<int>>
のような例をコンパイル時に正しくエラーにすることができます。ただこれにも問題がまだあります。
constexpr vector<vector<int>> vvi = {{1}};
int main() {
vector<int>& vi = vi[0];
vi = vector<int>{}
}
このコードは以前の要件の下でもエラーになります。std::vector
はconst
修飾されたメンバ関数からその要素への非const
な参照を取れないように巧妙に設計されているためです。これによってstd::vector
は多重ネストしてもstd::unique_ptr
のように内部要素を解放されてしまうことは起こりえません。すなわち、std::vector
はdeep constな型です。
しかし、新たな要件によるデストラクタのテストはネストしたstd::vector
を許可しません。std::vector
はconst
メンバ関数の慎重な設計によってdeep constとなっているだけでそのメンバは非const
のままであり、コンパイラはネストしたstd::vector
がdeep constであることを認識できません。
そこで、ユーザーに追加の作業を必要とせずにコンパイラが正しく型のdeep const性を認識するために、C++の型システムを拡張し新しいCV修飾子であるpropconst
を導入することを提案しています。
propconst
はポインタ型と参照型にのみ適用可能で、ポインタが不変である場合にconst
に変換され、それ以外の場合は何もしません。参照型はポインタ型に置き換えた上で同様です。
非メンバのオブジェクトポインタ型に対してpropconst
修飾している場合、その不変性はconst
修飾の有無で決まります。メンバ変数に対してpropconst
修飾している場合は呼び出すメンバ関数のconst
修飾によってその不変性が決定されます。
int propconst* ip1;
int propconst* const ip2;
struct S {
int propconst *ppi;
void f() const {
}
void f() {
}
};
最終的には、このpropconst
と以下の条件でもってNon-transientなメモリ確保を許可することが提案されています。
constexpr
なデストラクタの呼び出し(テスト)中に現れた全ての変数は、他のmutableな(実行時)文脈から到達可能ではない
constexpr vector<vector<int>> vvi = {{1}};
constexpr vector<unique_ptr<int>> vui = {std::make_unique<int>()};
propconst
はどこに現れるのかというと、std::vector
の実装に現れています。std::vector
の領域管理用のメンバ変数が全てpropconst
修飾されていれば、その要素を外部から変更可能でないことが保証可能であるため、コンパイラはstd::vector
がdeep constであることを認識可能です。つまり、普通のユーザーはpropconst
を意識せずともNon-transientなメモリ確保を利用できるようになります。
任意の型、テンプレートテンプレート...、非型テンプレートパラメータなど、テンプレートの文脈で使用可能なものを統一的に受けることのできるテンプレートパラメータ(Universal template parameter)の提案。
例えば高階メタ関数を書くときなど、引数として任意のものを受け取りたいことがよくあります。現状ではこれをするためにはそれぞれのテンプレートパラメータの種類毎の特殊化を行う必要があります。
template <template <typename...> typename F, typename... Args>
using apply = F<Args...>;
template<typename X>
struct G {using type = X;};
using OK = apply<G, int>;
template <template <typename> typename F>
using H = F<int>;
using NG = apply<H, G>;
これはまた、apply
の2番目以降の引数として非型テンプレートパラメータを渡そうとしても同じことが起きます。このような場合に、そのパラメータの種類を指定せずにテンプレートパラメータを宣言できるととても便利です。
提案では、2種類の文法を提案しています。
template <template <template auto...> typename F, template auto... Args>
using apply = F<Args...>;
template <template <__...> typename F, template auto... Args>
using apply = F<Args...>;
2番目の方法では同時に1つ目の方法も導入することになります。__
はパターンマッチにおける制約のないパラメータのようなものであり(switch
文のdefault
ラベルのようなもの)、そのパラメータに名前をつけることが出来ません。
template auto
はちょうど、C++17で導入されたauto
によるユニーバーサル非型テンプレートパラメータの宣言と同じようなことをします。
また、このUniversal template parameterを取るクラステンプレートをプライマリテンプレートとして、テンプレートパラメータの種類毎に特殊化を行えるようにすることも提案されています。
template <template auto>
struct X;
template <typename T>
struct X<T> {
using type = T;
};
template <auto val>
struct X<val> : std::integral_constant<decltype(val), val> {
};
template <template <typename> F>
struct X<F> {
template <typename T>
using func = F<T>;
};
現在のトランザクショナルメモリTS仕様の一部だけを、軽量トランザクショナルメモリとしてC++へ導入する提案。
トランザクショナルメモリはDBにおけるトランザクション処理の概念をDBをメモリに対応させ一般化したもので、トランザクション間においてメモリの一貫性を保証し、並行処理を容易に書くことができるようにするためのものです。
あるメモリ領域に対する1連の処理を1つのトランザクションとすると、そのトランザクションは成功するか失敗するかのどちらかであり、成功した場合にだけ結果がアトミックに書き込まれます。失敗した場合は進行中の処理は全てキャンセルされなされた変更はロールバックされるため、メモリ領域は一切変更されません。
このようなトランザクションはそれぞれがプログラム全体で1つの全順序に従うかのように実行され、あるトランザクションの実行中に外から処理中の状態を観測出来ず、1つのトランザクションは不可分の操作であるように実行されます。それによって、ユーザーはトランザクション間のデッドロックや同期などの心配を一切しないで並行処理を書くことが出来ます。
これらを標準機能として提供するために、トランザクショナルメモリTS仕様では2種類のトランザクション処理を定義するためのキーワード(transaction_relaxed/transaction_atomic
)とトランザクションキャンセル時の挙動を指定する2種類の指定子やある関数がトランザクション中で安全に扱えるかを指定する2種類の関数指定子(commit_on_escape/cancel_on_escape/transaction_safe/transaction_unsafe
)を定義していました。
2015年に現在のTS仕様が策定されていましたが実装もユーザー経験も少なく、機能がカバーする領域が広すぎると言う指摘もあり、標準への導入は見送られていました。そこでこの提案では、atomic
トランザクション(以前のtransaction_atomic
相当)とそのために必要な最低限の仕様変更だけをC++に導入することを提案しています。最終的にはTS仕様の全てを含めることを目指すために、まずは実装の負担にならない小さな変更から初めて行くつもりのようです。
導入されるキーワードはatomic
だけで、上記4つの指定子は全てありません。プログラムの実行に当たって発生するトランザクションはプログラム中で一貫した全順序によって実行され、同じ式を評価する2つのトランザクションは、先に評価が開始されたトランザクションの終了にもう一つのトランザクションの開始が同期します。
int f()
{
static int i = 0;
atomic {
++i;
return i;
}
}
int main() {
std::array<std::thread, 100> threads{};
for (auto& th : threads) {
th = std::thread([](){
int n = f();
});
}
for (auto& th : threads) {
th.join();
}
}
ただし、次のようにスレッド外部から観測可能な操作のatomic
ステートメント内部での実行は未定義動作とされています。
- I/O操作
volatile
領域へのアクセス
- atomic操作
そして、atomic
ステートメントの中では次の行いは実装定義です。
asm
宣言
- 到達可能な定義をもつ
inline
関数以外の関数の呼び出し
- 仮想関数呼び出し
- 関数名を指定しない後置式
throw
式
- コルーチン関連
- スレッドローカルストレージ及び静的変数の動的初期化
トランザクションのキャンセルはどうやらサポートされず、例外送出=キャンセルと考えればそれは実装定義のようです。また、その実装がハードウェアによるのかソフトウェアによるのかも規定していません。ほとんど実装定義です・・・
この部分は以下の方のご指摘によって構成されています。
多次元コンテナサポートのために添字演算子([]
)が複数の引数を取れるようにする提案。
行列や画像、位置情報など多次元のデータの1要素にアクセスするためには、ぞの次元に応じたインデックスが必要です。現状の添字演算子は1引数しか取ることができず、mdspan
やmdarray
などの多次元データ型でその要素にアクセスするためには関数呼び出し演算子(()
)を使用する必要があります。しかし、添字演算子と比べると明解ではなく少し混乱します。
そこで、添字演算子をオーバーロードする際に1つ以上の任意の数の引数を取れるように変更しよう、と言うのが提案です。この場合、関数呼び出し演算子との差異は引数なしオーバーロードが可能かどうかだけになります。
template<class ElementType, class Extents>
class mdpan {
template<class... IndexType>
constexpr reference operator[](IndexType...);
};
int main() {
int buffer[2*3*4] = { };
auto s = mdspan<int, extents<2, 3, 4>> (buffer);
s[1, 1, 1] = 42;
s(1, 1, 1) = 42;
}
C++20では添字アクセスの際に[]
の中にカンマを書くことが非推奨とされましたが、この提案の下では配列型の場合はC++17までのようなカンマによる式と常に認識し、クラス型の場合は添字演算子オーバーロードが見つからない場合にカンマによる式にフォールバックすると言う選択を取ることができ、C++17までの振る舞いをサポートし続けることが可能になります。
戻り値型を指定するstd::invoke
であるinvoke_r
の提案。
C++17以前から、関数呼び出しという操作を規格上で統一的に表現するためにINVOKE
という仮想操作があり、C++17ではそれに対応するライブラリ関数であるstd::invoke
が追加されました。
また、指定した戻り値型R
でINVOKE
するという操作もあり、対応するものとしてstd::invoke<R>
のような形で提案されていましたが、不要であるとしてドロップされました。
しかし、INVOKE<void>(f, args...)
のような呼び出しは戻り値を明示的に破棄するために便利です。また、std::is_invocable_r
やstd::is_nothrow_invocable_r
は指定した戻り値型で呼び出せるかを調べられるようになっており、std::visit
には戻り値型を指定するstd::visit<R>
が用意されています。
このように、やっぱり戻り値型を指定するstd::invoke
はあると便利なので追加しようという提案です。std::invoke_r
はstd::invoke
と比較して次のような利点があります。
void
を指定すれば戻り値を破棄できる
- callableオブジェクトの戻り値型を変換して呼び出しできる
- 例えば、
T&&
を返す関数をT
のprvalueを返す関数に変換できる
- 複数の戻り値型を返しうる呼び出しを指定した1つの型を返すように統一できる
namespace std {
template <class R, class F, class... Args>
constexpr R invoke_r(F&& f, Args&&... args)
noexcept(is_nothrow_invocable_r_v<R, F, Args...>);
}
[[nodiscard]] int f1(int);
std::invoke_r<void>(f1, 0);
template<typename T>
T&& f2(T);
int pr = std::invoke_r<int>(f2, 0);
struct base{};
template<typename F, typename... Args>
requires std::derived_from<std::invoke_result_t<F, Args...>, base>
base f3(F&& f, Args&&... args) {
return std::invoke_r<base>(std::forward<F>(f), std::forward<Args>(args)...);
}
効果としては、R
にvoid
が指定されたときは戻り値をstatic_cast<void>
して、それ以外は暗黙変換する、という感じです。
名前の_r
はstd::invoke
と間違えて使用しないようにするために付いています。
ポインタ経由のメンバアクセスの際に、->
だけでなく.
も使用できるようにする提案。
->
と.
はほとんど同じことをするのに使い分けが必要なのは最初にCを学ぶ時の混乱する点の一つであり、他のモダンな言語におけるメンバアクセスはほとんど.
で統一されています。また、ポインタと同等の振る舞いをする参照との間でのコードの非互換(コピペしたときに書き換えが必要)もあり、ポインタ経由のメンバアクセスに.
を許可しよう、というものです。
struct S {
int n;
operator int() {
return this.n;
}
}
int main() {
S obj{.n = 10};
S& ref = obj;
S* ptr = &obj;
ref.n = 20;
ptr.n = 20;
obj->n = 20;
}
これまでポインタに対しての.
はコンパイルエラーとなっていたので、この変更によって後方互換性が損なわれることはありません。
これは同時にC標準に対しても提案されています。
コロナウィルスの流行に伴ってC++標準化委員会の会議がキャンセルされている中で、リモートに移行しつつどのように標準化作業を進めていくのかをまとめた文章。
今後のテレビ会議のカレンダーとかリアル会議で何してるのかとか、リモートやメール等の代替手段でどう作業するかみたいなことが書いてあります。
には、C++23以降の優先度の高いライブラリと言語機能についての進捗等のまとめが書かれています。
今の所は「P0592R4 To boldly suggest an overall plan for C++23」によって示された予定の変更はないようですが、既に11月のニューヨークで行われる予定だった会議もキャンセルされているので、さすがに変更があるかもしれません・・・
Numbers TS (P1889R1)に対して、10進多倍長浮動小数点型std::decimal
を追加する提案。
P1889R1は将来C++に導入することを目指した数値型関連の提案をまとめたもので、多倍長整数型などが提案されています。現状10進浮動小数点型は無いようなので追加しようということのようです。
現在のMutexライブラリ周りの文言にはいくつか問題があるのでそれを修正するための提案。
主に以下のような問題に対処するものです。
std::shared_lock<Mutex>
のパラメータMutex
はshared mutex
要件を満たすことが要求されているが、その参照先はshared timed mutex
になっている。この不一致によって、たとえユーザーが時間指定して待機する関数を呼ばなかったとしてもshared_lock<shared_mutex>
は未定義動作となりうる。
std::shared_lock
の現在の表現は内部定義(規格に表されていない?)を参照しているため、ユーザー定義の共有Mutex型の利用が許可されていない。これは明らかな欠陥。
- Lock関連の操作全般の文言に横たわる問題として、ロック操作の事前条件を基礎となるロック可能な操作の事前条件と混同したり、ロック可能であることをミューテックスと混同する問題がある。
これらの問題は文言や要件の不足によるものなので、必要な要件を追加し文言を整理・調整する事で解決を図っています。
Networking TSのassociated_executor
からデフォルトのExecutorを取り除く提案。
前回公開されたこの提案から派生したもののようです。
associated_executor
は非同期処理の完了時に呼ばれるハンドラ(コールバック)に関連づけられたExeutorで、あるハンドラはそのassociated_executor
で指定されたExecutor(および実行コンテキスト)で実行されます。これはユーザーによってカスマイズ可能にするために用意されており、デフォルトではsystem_executor
が使用されることになっています。
ただ、system_executor
はいくつかの特異な性質を持っています。例えば、system_context
は受け取った処理を任意の数並列実行することが許可されています(すなわち、スレッドプールを想定している)。これを使用するExecutorを選択する場合ユーザーには強い並行性要件が課されます。system_context
はsystem_executor
の実行コンテキストであり、暗黙のうちにこれにフォールバックすると静かにデータ競合(未定義動作)を引き起こします。
一方で、io_contex::run()
など投入した処理は現在のスレッドをブロックして実行され、ユーザーが処理が実行されている時とされていない時を制御可能な実行コンテキストをデフォルトで使用するものもあります。これらはそれぞれ別々の場所で使用されており、ユーザーがそのつもりもないのにsystem_executor
に静かにフォールバックする場合、全く意図しない偶発的なデータ競合を引き起こしてしまいます。
また、system_context
は投入されたワークアイテムの生存期間(lifetime)を任意に延長することが許されています。処理の前方進行を停止する方法も提供されてはいますが、ワークアイテムの寿命が確実に尽きることを保証する方法はありません。対照的に、ユーザーはワークアイテムの生存期間がいつ終了するかを制御可能なExecutorを使用することができます。その場合に意図せずsystem_executor
にワークアイテムを投入してしまうと、あらゆる種類の生存期間にまつわるバグを引き起こす可能性があります。
特に、これらの性質のそれぞれはsystem_context
のシングルトンオブジェクトがグローバル変数であるということに由来しています。
これらの問題を抱えているものをデフォルトに据えておくのは明らかにバグの元であるので削除しよう、ということのようです。
std::variant
を公開継承している型に対してもstd::visit()
できるようにする提案。
namespace std {
template <class R, class Visitor, class... Variants>
constexpr R visit(Visitor&& vis, Variants&&... vars);
}
std::visit
は上記のように宣言されていますが、例外指定の文言において「vars
に含まれる全てのstd::variant
が...」のように指定されていることから、Variants
パラメータパックに含まれてもいいのはstd::variant
の特殊化だけ、となっています。
文言の調整によってこれを緩和し、std::varinat
(の任意の特殊化)を曖昧でないpublic
な基底クラスとして持つ型でも呼べるようにしようとしているものです。
これはすでにClang(libc++)とMSVCには実装済みのようなので、欠陥報告(C++17?)として採択されそうです。
言語サポートのあるより自然に使えるnative tupleを追加する提案。
native tupleは山かっこ<>
のなかに型名を書くことによって導入し、{}
(braced init list)によって初期化されます。要素アクセスはnative tupleオブジェクトnt
に対してnt.<I>
の様に行われ、これはstd::tuple
のオブジェクトlt
に対するstd::get<I>(lt)
と等価の働きをします(I
は型名でもok)。
<int, double> t1 = {1, 1.0};
int a = 0;
auto t2 = {a, "str"s};
<int&, double> t3 = {a, 0.5};
t2.<0> = 1;
t3.<0> = 2;
auto d = t3.<double>;
スライシングと展開
<int, double, int, std::string> t = {1, 2.0, 3, "4.0"};
auto slice = t.<1..2>;
auto t2 = {...t...};
クラステンプレートの型推論で利用
std::map m = {
{1, 2.3}, {3, 4.5}, {6, 7.8}
};
多値返却関数
auto f() -> <int, double> {
return {1, 2.0};
}
auto f() {
return {1, 2.0};
}
主に{...}
がstd::initializer_list
に推論されてしまう事から起きている一貫性の無さと不便さを解決することを目的としている様です。しかし、どう見てもstd::initializer_list
と衝突しているのでさらに検討が必要そうです。
元のシーケンスの各要素にインデックスを紐付けた要素からなる新しいシーケンスを作成するRangeアダプタviews::enumrate
の提案。
std::vector days = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"};
int index = 0;
for(const auto& d : days) {
std::cout << std::format("{} {} \n", index, d);
index++;
}
for(const auto& [index, d] : std::views::enumerate(days)) {
std::cout << std::format("{} {} \n", index, d);
}
範囲for
でインデックスが欲しい時は本当によくあるけれどそのままだと取れないため、外部スコープでインデックスを定義してインクリメントしたり普通のforループが使用されたりします。これは冗長でバグの元であるため、単純なライブラリ機能で解決が可能なviews::enumrate
を追加しようというものです。また、すでにrange-v3
やboost::range
には同等のものが実装されています。
参考実装が等価なfor
ループと同等のコードを出力している結果が掲載されています。
https://godbolt.org/z/2Kxo8d
std::pair
と2要素std::tuple
の間の非互換を減らし比較や代入をできるようにする提案。
std::pair
と2要素のstd::tuple
は本質的に同じものであり多くのインターフェースを共有していますが、std::tuple
からstd::pair
への代入ができなかったり互いに比較ができなかったりと非互換な部分があります。そうした非互換を取り除きよりstd::tuple
とstd::pair
の一貫性を向上させるのが目的です。
constexpr std::pair p{1, 3.0};
constexpr std::tuple t{1.0, 3};
t = p;
p = t;
bool b1 = P == t;
bool b2 = (p <=> t) == 0;
これらの事は、既存のstd::tuple
のコンストラクタと比較演算子を変更し、コンセプトによってtuple-likeなオブジェクトを受け入れ可能にすることで達成されます。そのため、std::pair
でできるようになる上記の事はより一般のtuple-like(pair-like)な型でも同時に可能になります。
contextually convertible to boolと言う規格上の言葉を、C++20で定義されたboolean-testable
コンセプトを使用して置き換える提案。
純粋に規格の言葉の変更なので一般ユーザーには関係ないはずです。
contextually convertible to boolと言う要件はざっくりいえば「標準ライブラリが求めるときにbool
に変換できること」みたいな意味で、比較演算子の戻り値型や述語関数の戻り値型に要求されるものです。この要件を言葉で式に対して定義するのが難しかったらしく、長年紛糾していたようです(LWG Issue 2114)。
C++20では当初あったboolean
コンセプトが置き換えられてboolean-testable
という、まさにそれを表現するコンセプトが導入されました。そこで、これを使ってcontextually convertible to boolという要件を規定しようとしているようです。
Rangeライブラリと連携可能なT
型の要素列を生成するコルーチンジェネレータstd::generator<T>
の提案。
これは直接シーケンス列を生成するものではなくて、その様な処理をコルーチンによって書くときに使用可能なawaitableなクラスです。
std::generator<int> fib (int max) {
co_yield 0;
auto a = 0, b = 1;
for(auto n : std::views::iota(0, max)) {
auto next = a + b;
a = b, b = next;
co_yield next;
}
}
int answer_to_the_universe() {
auto coro = fib(7) ;
return std::accumulate(coro | std::views::drop(5), 0);
}
この様な同期ジェネレータは多くのケースで有用であり基本的なコルーチンを書くためには必須だけど、正しく効率的に実装するのは難しいので標準で用意しよう、という提案です。
宣言以降使用されず追加情報を提供するための名前をつける必要もない変数を表すために_
を言語サポート付きで使用できる様にする提案。
std::lock_guard _(mutex);
auto [x, y, _] = f();
inspect(i) {
1 => 0;
_ => 1;
};
これは関数スコープでの_
という変数名を少し特別扱いして、暗黙的に[[maybe_unused]]
を付加し、再定義された時でも静かに実装定義の別名に置換した上で同じことをし名前探索の候補に上がらない様にするものです。
名前空間スコープでは基本的に使用できませんが、モジュールファイル実装単位(の本文)内でだけは関数スコープと同様に使用可能です。
namespace a {
auto _ = f();
auto _ = f();
}
void f() {
auto _ = 42;
auto _ = 0;
{
auto _ = 1;
assert( _ == 1 );
}
assert( _ == 42 );
}
使用可能な場所に制限はありますが、これによって既存のエンティティ名に_
を使っているコードを壊さずにこの振る舞いを導入することが可能になるはずです。
静的例外のために追加されるstd::error
について、実装経験に基づいた設計に関するフィードバックの文書。
実装リポジトリ
github.com
std::error
は任意のエラーを表現する型を型消去によって統一的に扱う型で、型消去したエラーオブジェクトとその復元のための情報を持つポインタで構成され、ポインタ2つ分を超えない程度のサイズを持ちます(std::string_view
と同等)。主にstd::exception_ptr
を取り扱う事を想定していますが、std::shared_ptr
等を用いてより大きなサイズを持つエラー型を扱う事も出来るようです。
この提案で主に述べられているのは従来のエラー型をstd::error
にマッピングする際の問題点です。
std::error_code
をstd::error
にマッピングするとそのサイズの都合(std::error_code
だけでポインタ2つ分のサイズがある)で効率的でなくなる。
std::errc
はゼロ(デフォルト構築)相当の値がエラーなしを表現するがstd::error
は常にエラーだけを表現するためそこのマッピングをどうするか?
std::error
はエラー値の意味論的な等値比較が可能とされているがstd::errc
と現在の標準の動的例外型を完全には対応付けできない。
等の問題についての報告がなされています。
現在のNetworking TS(N4771)のベースとなっている規格はC++14なので、C++20ベースに更新する提案。
標準ライブラリのモジュール化について急ぐべきか疑問を投げかける文書。
モジュールには主に以下の利点があります。
- コンパイル時間の改善
- ODR違反の緩和
- 実装詳細の分離
- マクロの分離
一方、標準ライブラリには次の制約があります。
#include
をサポートし続ける必要がある
- ABIを破壊しない
- 複数の言語バージョンをサポートする必要がある
この制約の下では標準ライブラリのモジュール化にはそれほど恩恵が無いため、モジュール化するにしても優先度は低く、とりあえずはヘッダユニットのインポートで済ませて他の事に時間を割いた方が良いのでは?というのが著者の主張です。
著者のおすすめ
hidden friends
の観点から、既存の演算子オーバーロードやカスタマイゼーションポイントを調整する作業を優先する。
- その上で、大きめの粒度のモジュールを優先する。
- フリースタンディングとモジュール化は直交しており、フリースタンディングは最適なモジュールの構成方法を示唆している。少なくとも部分的にフリースタンディングであるような機能は同じモジュールにあるべき。
- 全てのヘッダをモジュール化する事を目指さない。条件付きコンパイルに使用される機能(
<cassert>, <version>
)は使用される際に明示的にインクルードされるべき。
- コンパイル時間に関する提案のコストを決定するために標準ヘッダがインポートされるものとする
また、モジュールの大きさがどうあるべきかやC++23以降に追加される新しいライブラリ機能をモジュールにだけ追加するようにすることについても考察されています。
前者は、依存関係が多い場合には小さすぎると分割の意味をなさず、大きすぎると並列コンパイルの機会を喪失するが、大きい方がimport
の数を減らせるためコンパイル時間削減には有利と述べられており、後者については、ABIの安定性の観点から難しいだろうと述べられています。
ラムダ式の関数呼び出し演算子に対して属性指定を出来るようにする提案。
関数オブジェクトを手で書いた場合は属性指定を行えるのだからラムダ式に対しても行えるようにするのは妥当なのでそのようにしよう、という提案です。
その場合問題になるのがどこに属性指定を置くかです。現在ラムダ式に対しての属性指定はnoexcept
の後ろ、後置戻り値型の前に置くことができて、これは関数呼び出し演算子の型(メンバ関数型)に対して適用されています。文法的には、ラムダ導入子([]
)の前、ラムダ式全体の後ろ(;
の前)、ラムダ導入子([]
)の直後という候補があるようですが、この提案ではラムダ導入子([]
)の直後を採用しています。
auto lm1 = [](int n) noexcept [[nodiscard]] -> int { return n; };
auto lm2 = [][[nodiscard]](int n) noexcept -> int { return n; };
ラムダ導入子([]
)の前は構文に曖昧さをもたらすため不適切で、ラムダ式全体の後ろは将来的にクロージャ型そのものに対して属性指定できるようにする場合のために残しておくことにした結果、ラムダ導入子([]
)の直後が選ばれました。
この部分は以下の方のご指摘によって構成されています。
C99から存在している複合リテラル(compound literal)をC++でもサポートする提案。
複合リテラルは(型名){初期化子}
の構文で指定された型のオブジェクトをその場に生成します。
int n = (int){10};
auto&& ar = (double[]){1.0, 2.0, 3.0};
C99の複合リテラルは常にlvalueを返すようですが、この提案ではprvalueを返すようにされています。既存のT{...}
との一貫性を重視したもので、確かにC++的にはそちらの方が自然でしょう。
auto& ref = (int[]){0, 1, 2, 3, 4, 5};
const auto& cref = (int[]){0, 1, 2, 3, 4, 5};
auto&& rref = (int[]){0, 1, 2, 3, 4, 5};
著者は、T{...}
の型名をかっこで囲んで(T){...}
としても同じ結果が得られるのは自然であり、CがすでにそれをサポートしているためGCCやclangでも使用可能であるが実装間に差異があるので、標準化することでより扱いやすくなるだろう、と述べています。
次
onihusube.hatenablog.com
この記事のMarkdownソース