Rokiのチラ裏

学生による学習のログ

Synchronized Buffered Ostream

先日、米国のニューメキシコ州アルバカーキで開催された ISO C++ 委員会による国際会議にて C++20 のドラフトに追加された Synchronized Buffered Ostream (p0053) についてのメモ。当エントリー内容は同提案書である p0053 に基づく。

C++ の一部のストリーム出力操作は競合しない事が保証されているが、その効果が実用的である事は保証されていない。C++20 から同提案によって、異なるスレッドからの出力をコヒーレントな順序で実行できるようになった。導入されるのは、std::basic_streambuf をラップした std::osyncstream (std::basic_osyncstream<char>) と std::wosyncstream (std::basic_osyncstream<wchar>) *1である。std::basic_osyncstreambasic_osyncstream::emit メンバ関数が呼び出された時、または破棄時*2に内部ストリームバッファの内容を basic_ostream のストリームバッファにアトミックに転送する。挙動は以下の通り、実際の出力やフラッシュのタイミングが少し複雑。

#include <syncstream>
#include <iostream>

{
    std::osyncstream bout{std::cout}; // std::cout の同期ラッパー
    bout << "Hello, " << "World!" << ’\n’;

    std::osyncstream{std::cout} << "The answer is " << 6*7 << std::endl; // 同じくstd::cout の同期ラッパー。フラッシュ操作だがこの時点ではフラッシュはされず、まだ出力されない。
    bout << "and more !\n";
} // 文字列がアトミックに転送され、std::cout がフラッシュされる

{
    std::osyncstream bout{std::cout};
    bout << "Hello, " << "World!" << '\n';
    bout.emit(); // 文字列が転送される。std::cout はフラッシュされない。
    bout << "and more!" << std::endl; // フラッシュ操作、しかしまだフラッシュされない。
    bout.emit(); // 文字列が転送される。std::cout がフラッシュされる。
    bout << "and...\n"; // フラッシュ操作はなし
} // 文字列が転送される、std::cout はフラッシュされない。

内部実装として、代入演算子の呼び出しにおいても emit メンバ関数が呼び出されるようだが、ラップされたストリームバッファから rhs の関連付けを解除して、rhs の破壊が出力を生成しないようにするようだ*3。 尚執筆時現在まだ取り込まれていないが、p0753r1 が追加されれば、以下のように osyncstream, wosyncstream のマニピュレータを利用してフラッシュ時の挙動を制御できるようになる。

{
    // static_cast<basic_osyncbuf<charT, traits, Allocator>*>(basic_ostream<charT, traits>().rdbuf())->set_emit_on_sync(true) が呼ばれる
    std::osyncstream{std::cout} << std::emit_on_flash << "foo" << std::endl; // 文字列が転送される。std::cout はフラッシュされる。
}
{
    // static_cast<basic_osyncbuf<charT, traits, Allocator>*>(basic_ostream<charT, traits>().rdbuf())->set_emit_on_sync(false) が呼ばれる
    std::osyncstream{std::cout} << std::noemit_on_flash << "var" << std::endl; // 文字列は転送されない。まだフラッシュされない。
} // 文字列が転送される、std::cout がフラッシュされる。
{
    // static_cast<basic_osyncbuf<charT, traits, Allocator>*>(basic_ostream<charT, traits>().rdbuf())->emit() が呼ばれる
    std::osyncstream{std::cout} << "foo" << std::flush_emit; // 文字列が転送され、フラッシュされる。
}

p0053 本文にはこのような設計となっている目的として以下のように述べられている。本文から引用。

Can you make a flush of the osyncstream mean transfer the characters and flush the underlying stream?

No, because the point of the osyncstream is to collect text into an atomic unit. Flushes are often emitted in calls where the body is not visible, and hence unintentionally break up the text. Furthermore, there may be more than one flush within the lifetime of an osyncstream, which would impose a performance loss when an atomic unit of text needs only one flush. The design decision is only to flush the underlying stream if the osyncstream was flushed, and only once per atomic transfer of the character sequence. p0053r5 introduced manipulators to allow both ways, but the default remains not to flush immediately.

Can flush just transfer the characters and not flush the underlying stream?

The flush intends an effect on visible to the user. So, we should preserve at least one flush. The flush may not be visible to the code around the osyncstream, and so its programmer cannot do a manual flush, because attempting to flush the underlying stream that is shared among threads will introduce a data race.

尚、本提案のセクション 3.8 に記載されている実装のサンプルコードは、筆者によって若干の変更が加えられた

*1:直接的なラップは std::basic_syncbuf で行われる

*2:内部実装としてはデストラクタで emit メンバ関数が呼び出される

*3: [syncstream.syncbuf.assign]

Range-based for statements with initializer

先日、米国のニューメキシコ州アルバカーキで開催された ISO C++ 委員会による国際会議にて C++20 のドラフトに追加された Range-based for statements with initializer (p0614) についてのメモ。当エントリー内容は同提案書である p0614 に基づく。C++17 では if 文でも、 condition の前に  init-statement を書けるようになったが、それと似たようにして、C++20 では*1 Range-based for の  for-range-declaration の前に  init-statement が書けるようになる。p0614r0 から引用。

Insert a new paragraph at the end of subsection [stmt.ranged].
A range-based for statement of the form
for ( init-statement for-range-declaration : for-range-initializer ) statement
is equivalent to:
{
  init-statement
  for ( for-range-declaration : for-range-initializer ) statement
}

この構文が提案された動機付けの殆どは p0305 で述べられている。本文では、 for-range-declaration type-specifierauto& であり、 for-range-initializer 内でテンポラリオブジェクトが生成された場合、ライフタイムは延長されず結果として dangling reference となってしまうため、これを避ける用途としても利用できると述べられている。p0614r0 から引用。

For example, consider the following simple type that exposes an internal collection:
class T {
  std::vector<int> data_;
public:
  std::vector<int>& items() { return data_; }
  // ...
};
Consider further a function returning a prvalue:
T foo();
Even users who are familiar with the intricacies of prvalue lifetime extension, and who would be confident about a hypothetical statement
for (auto& x : foo()) { /* ... */ }
can easily fail to spot that the similar looking
for (auto& x : foo().items()) { /* ... */ }
has undefined behaviour. While this particular pitfall will presumably stay with us for the foreseeable future (but see below for further discussion), the proposed new syntax will at least allow users to write correct code that looks almost as concise and local as the wrong code above:
for (T thing = foo(); auto& x : thing.items()) { /* ... */ }
Note that we are not proposing that the init-statement be in the same declarative region as any later part of the statement. In other words, for (auto x = f(); auto x : x) is valid and the outer x is hidden in the loop body. This is consistent with the proposed rewrite rule; in the current standard, T x; for (auto x : x) is already valid.

また、上記のようにテンポラリオブジェクトの生成によって dangling reference を持ってしまう問題は Range TS や Boost Range 、その他パイプラインで繋げるような range adaptors の形式をとっているアルゴリズムセットの動作に、直接的に影響を及ぼす*2

for (auto& x : f().filter(pred).top(10).reverse()) { // how many temporaries?
  mutate(&x);
}

range expression 内の全ての一時的な値がループの終わりまで生き残る特有のセマンティックスを与える事で dangling を防ぐという手段も考案されているようだが、この提案はそれとは異なった方法で解決を試みる事ができるとしている。事実、以下のように記述すれば、ループの終わりまでオブジェクトは生き残る。

for (T container = f(); auto& x : container.filter(pred).top(10).reverse()) {
  mutate(&x);                  // ^^^^^^^^^ ^^^^^^^^^^^^ ^^^^^^^ ^^^^^^^^^
}                              // lvalue    prvalue      prvalue prvalue

なおこの変更による影響についても、同提案内で言及されている。まず標準に対する影響であるが、これは主要な言語拡張であるので、すなわち現在の標準では不正な構文であるので、この変更そのものが標準ライブラリなどに対する設計に影響を与える可能性は低いと述べられている。次にこの構文の実装に対してであるが、様々な実装者が、この提案は一定の実装上の課題を提起するかもしれないと報告したが、原則的かつ合理的に現実で実行可能でなければならない。また、新しい構文では、range-based を通常の for 文と区別するのが難しくなり、2つを区別するためにより洗練された解析が必要になるだろうと述べられている。また 2015 Kona 会議で発表された p0026 multi-range iteration を踏まえて継続的に設計が必要である旨を述べている。multi-range iteration とは、

for (auto x : a; auto y : b; auto z : c) {
  f(a, b, c);
}

というように記述し、範囲 a, b, c を lock-step, すなわち zip の順序で繰り返す事のできる拡張構文である。現在この提案については進展しておらず、不等長の範囲の扱い方などに関する技術的問題を抱えているが、それはライブラリで解決できる問題であるとのこと。Range-based for statements with initializer は、この提案と互換性があるため p0026 が採択された場合においても多大な変更を伴う必要はないとのこと。例えば、以下のようにサポートできるとのこと。

for (T thing = f(); auto x : a; auto y : b; auto z : c) {
  thing.bar(a, b, c);
}

最後に今後の方向性が述べられており、Range-based for にインクリメント機能をつける(for(init; auto x:e; inc))というものや、do ... while 文にて初期化文を書けるようにするといったものが述べられている(do (init;) statement while (expr);)。

*1:執筆時

*2:この問題は CWG 900, CWG 1498にて取り上げられている。

Conditionals with Omitted Operands

GCC の Conditionals with Omitted Operands という拡張構文についてのメモ。x ? x : yというような Conditional operator を利用した記述を次のように記述する事ができる。

x ? : y

値は x が 0 でない限り x 、そうでない場合 y である。このような単純なケースでは上記のように中間のオペランドを省略する事は特別有用ではないが、最初のオペランドが副作用を含む場合、直接二度記述すると当然ながらその副作用が二度発生する。上記のようにオペランドを省略する事で、望ましくない副作用を防止する事ができるとのこと*1。しかし、あくまで GCC による独自拡張の構文であるため、利用には移植性などの観点をふまえること*2

たんぶらーくらいあんと

github.com

二ヶ月半ほど前に一度 Electron を触っておこうと思い作り始め、二ヶ月前にもう作るのに飽きてしまったたんぶらーくらいあんとについてブログに特別載せていなかったので一応リンクを載せておく事にした。一応上記リポジトリから動いている様子も見れるはず。実装は React + Typescript + Electron + SCSS で行なっていて、OAuth 認証からリブログ、ライクのなどの基本的な操作と、J, K キーによるスクロールアップダウン、可変レイアウトなどが実装済みではあるが、動画、音楽、引用文などのコンテンツはまだダッシュボード上に流れて来ていたとしても無視する。スクロールアップダウンを作るあたりで飽きてしまった...そのうち完成させたい。

React, Typescript と言えばつい二ヶ月ほど前に個人サイトを新しくした際にも使った。一度使うと、もう生の html とかは書く気が起きない。

youtu.be

ビルトインを使わずに任意の型のアラインメントを計算

alignment_of は C++11 で標準化されており、殆どの場合はビルトイン関数を呼び出す実装となっているが、ふと Boost の実装を見るとなるほど〜という感じだったのでメモ。Boost による実装は今からかな〜り前からあるので、今更感はあるが。

// 以下の実装は、より簡略化/多少の改変が施されている事に留意されたし

template <class T>
struct alignment_hack {
    char c;
    T t;
    alignment_hack() = delete;
};

template <unsigned A, unsigned S>
struct alignment_logic 
    : public integral_constant<std::size_t, A < S ? A : S> {};

template <class T>
struct alignment_of
    : public integral_constant<std::size_t, alignment_logic<sizeof(alignment_of_hack<T>) - sizeof(T), sizeof(T)>::value> {};

alignment_hack で、任意のタイプT*1の前に、1 バイトを置いているのが肝である。実際に型を入れてメモリ空間を想像すると分かりやすい*2。例えば、T が int32_t/uint32_t である場合、alignment requirement*3 に則って alignment_hack::t の後方に 3 バイトのパディングを入れ alignment_hack が 8 バイトとなるため、そこから差し引いてアライメントの値である 4 を得られる。次に、struct X{ char a[5]; } を当てはめたとしよう。この場合、パディングは挿入されないのでそのままsizeof(X)の値を引いてアライメントの値 1 を取得する事ができる。

この実装は必ずしもアライメントの実数値を確実に取得できるものではないという事に留意しなければならない。保証されているのは、アライメントの値の倍数である太字で強調したのは、パディングがどの部分に取られるか規定されているわけではないためである。また、vptr などがどこに挿入されるかなども推測できるものではない。殆どの実装では大抵メンバの後ろに挿入されるため、よくあるケースではないが、実際に起きたケースも報告されている

このトリックを利用した自作ライブラリの方の実装は以下の通りである。

*1:簡略化のためこの例では全ての型が意図した通りに動かない。例えば、参照型、 void、long double などは、特殊化が必要となる。

*2:論理的な観点からの説明は以前書いた文書を参照。

*3:6.7.1 Fundamental types/1

lua de gmail

単なるメール送信のためにメールクライアントを起動したりブラウザを起動したり、sendmail コマンドを設定するのは面倒だったので lua でそのような用途の簡易的なスクリプトを書いたのだが、lua を書く事自体本当に久しぶりだった。Gist に上げるまでもないが、どこかに置いておきたかったので、取り敢えずブログに貼り付ける事とした。

-- readConf.lua
local json = require "cjson"
local lfs = require "lfs"
local doc_path = "./"

function readConf()
    val = {}
    local result, file
    
    for filename in lfs.dir(doc_path) do
        result = string.find(filename, "mailconf.json")
        if result ~= nil then
            file = io.open(filename, "r")
            io.input(file)
            val = json.decode(io.read())
            io.close(file)
        end
    end
    return val
end
-- sendMail.lua
local socket = require "socket"
local smtp = require "socket.smtp"
local mime = require "mime"
local ssl = require "ssl"
local https = require "ssl.https"
local ltn12 = require "ltn12"

local function headerEncode(src)
    return "=?UTF-8?B?" .. mime.b64(src) .. "?="
end

local function sslCreate()
    local sock = socket.tcp()
    return setmetatable({
        connect = function(_, host, port)
            local r, e = sock:connect(host, port)
            if not r then return r, e end
            sock = ssl.wrap(sock, {mode = "client", protocol = "tlsv1"})
            return sock:dohandshake()
        end
    }, {
        __index = function(t, n)
            return function(_, ...)
                return sock[n](sock, ...)
            end
        end
    })
end

function sendMessage(j, to_receiver, subject, body)
    local msg = {
        headers = {
            from = headerEncode(j.Name) .. " <" .. j.UserName .. ">",
            to = headerEncode(to_receiver["Name"]) .. " " .. to_receiver["Address"], 
            subject = headerEncode(to_receiver["Subject"]),
            ["content-type"] = 'text/plain; charset="utf-8"'
        },
        body = to_receiver["Body"]
    }
    local ok, err = smtp.send {
        from = "<".. j.UserName .. ">",
        rcpt = "<" .. to_receiver["Address"] .. ">",
        source = smtp.message(msg),
        user = j.UserName,
        password = j.Password,
        server = "smtp.gmail.com",
        port = 465,
        create = sslCreate
    }
    if not ok then
        print("Mail send failed ", err)
    end
end
-- main.lua
dofile("sendMail.lua");
dofile("readConf.lua");

sendMessage(readConf(), {Name = "xxx 様", Address = "example@mail.com", Subject = "件名", Body = "こんにちは"})

以下のような json ファイルから設定を読み込む。

{ "UserName": "example@example.com", "Password": "password", "Name": "watashi" }

なんだか懐かしい。

P0532R0: On launder() 和訳

std::launder については以前触れたP0532R0: On launder() にとても分かりやすくまとめられていたので、個人的理解のためも含めて和訳してみる。

C++17では、C++14 の NB コメントの結果としてstd::launderを導入している:C++ FCD Comment Status
それは Core Issue 1776 として議論された*1http://www.open-std.org/jtc1/sc22/wg21/docs/cwg_defects.html#1776
最初の具体的な提案:Pointer safety and placement new
最終的に受け入れられた wording:Core Issue 1776: Replacement of class objects containing reference members (et al)
この問題は、C ++17 の次のNBコメントにも影響する:JP20, CA12
このペーパーでは、まずstd::launderが解決しようとしている問題(何十年も存在していた問題)を正確に説明しようとしている。また、私の意見では、何故別の解決策がより理にかなっているのかを動機付けるために、std::launderを用いた解決策から生じた問題についても議論する。要約すると、定数/参照メンバが有効な要素に対して、std::launderを使用して変更することなく、現在配置newが存在する既存のコードを有効にすることを強く推奨する。

Which Problem shall launder() solve?

現在の標準によると、次のコードは未定義の動作になる。
struct X {
 const int n;
 double d;
};
X* p = new X{7, 8.8};
new (p) X{42, 9.9}; // request to place a new value into p
int i = p->n; // undefined behavior (i is probably 7 or 42)
auto d = p->d; // also undefined behavior (d is probably 8.8 or 9.9)
理由は、現在のメモリモデルにある。
3.8 Object lifetime [basic.life]
オブジェクトの存続期間が終了した後、オブジェクトが占有したストレージが再利用または解放される前に、元のオブジェクトが占有した記憶領域に新しいオブジェクトが作成され、元のオブジェクトが占有していた記憶領域、元のオブジェクトを指し示すポインタ、または元のオブジェクトの名前が自動的に新しいオブジェクトを参照し、新しいオブジェクトの存続期間が開始されたら、次の条件を満たす場合に新しいオブジェクトを操作するために使用する事ができる:
  • 新しいオブジェクトの記憶域は、元のオブジェクトが占有していた記憶場所を正確に overlay する
  • 新しいオブジェクトは元のオブジェクトと同じ型である(top-level の cv 修飾は無視される)
  • 元のオブジェクトの型はconst修飾されていない。クラス型の場合は型が const 修飾された非静的データメンバーまたは参照型を含んでいない
  • 元のオブジェクトはT型の最も派生したオブジェクトであり、新しいオブジェクトはT型の最も派生したオブジェクトである(つまり、それらは基本クラスのサブオブジェクトではない)
標準で対応する一つの例を示す。
1.8 The C++ object model [intro.object]
[ Example:
struct X { const int n; };
union U { X x; float f; };
void tong() {
 U u = {{ 1 }};
 u.f = 5.f; // OK, u の新しいサブオブジェクトを作成する
 X *p = new (&u.x) X {2}; // OK, uの新しいサブオブジェクトを作成する
 assert(p->n == 2); // OK
 …
 assert(u.x.n == 2); // 未定義動作、u.xは新しいサブオブジェクトの名前を付けない
} 
—end example ]
この動作は新しいものではなく、C++03 以来存在している。標準の対応する文言の結果として、定数または参照メンバを持つ可能性のあるオブジェクトの配置 new は定数メンバーまたは参照メンバーを持つ可能性のあるオブジェクトに使用され、placement new の戻り値は常にメモリへのアクセスごとに使用される事が保証される:
struct X {
 const int n;
};
X* p = new X{7};
p = new (p) X{42}; // pに新しい値を置くことを要求する
int i = p->n; // OK、p が新しい placement new の戻り値によって再初期化されたため i は 42 である。 

Why using the return value of placement new is a problem

残念ながら、実際には、placement new の戻り値を常に簡単に使用できるわけではない。追加のオブジェクトが必要な場合があり、現在のアロケータインターフェイスはこれをサポートしていない。

Placement new into members

戻り値を使用する際にオーバーヘッドとなる原因の1つの例は、ストレージが既存のメンバーである場合である。std::optionalstd::variantの場合があてはまる。
template <typename T>
class coreoptional
{
private:
    T payload;
public:
    coreoptional(const T& t)
        : payload(t) {}

    template<typename... Args>
    void emplace(Args&&... args) {
        payload.~T(); 
        ::new (&payload) T(std::forward<Args>(args)...); // *
     }
     const T& operator*() const & {
         return payload; // **
     }
}; 
ここでTが定数または参照メンバを持つ構造体である場合
struct X {
    const int _i;
    X(int i) : _i(i) {}
    friend std::ostream& operator<< (std::ostream& os, const X& x) {
        return os << x._i;
    }
};
次のコードは、未定義の動作となる。
coreoptional<X> optStr{42};
optStr.emplace(77);
std::cout << *optStr; // 未定義の動作(恐らく 42 または 77 が出力される)
その理由は、出力操作では演算子*(上記の**を参照)が呼び出され、戻り値を使用しない新しい値が配置されたメモリが使用される(*上記参照)。このようなクラスでは、placement new の戻り値を保持するポインタメンバーを追加する必要があり、値が必要な時に利用する。
template <typename T>
class coreoptional
{
private:
    T payload;
    T* p; // placement new の戻り値を使用できるようにする。
public:
    coreoptional(const T& t)
        : payload(t) {
        p = &payload;
    }
    template<typename... Args>
    void emplace(Args&&... args) {
        payload.~T();
        p = ::new (&payload) T(std::forward<Args>(args)...);
    }

    const T& operator*() const & {
        return *p; // ここで payload を使ってはならない
    }
};
このオーバーヘッドを避けるためにstd::launderが導入された(下記参照)。

Placement new into allocated memory

データへのアクセスが必要なときに使用されるメモリへのポインタを内部的に保持しているため、ポインタメンバーを追加するこのオーバーヘッドはヒープ上にメモリを割り当てる(コンテナ)クラスにとって問題とはならないと言える。しかし、それに限らずいくつかの異なる問題を抱えている。まず、アロケータが使用されているとき、ここでは new の返り値を使用することはできない。なぜなら、現在アロケータインタフェースは new の返り値を扱う手段を提供していないからである。アロケータ要件(17.5.3.5 Allocator requirements [allocator.requirements])に従うと、a.construct(c, args)は戻り値の型がvoidであるため、placement new の戻り値を使用する事はできないのである。従って、現在(C++14まで)、定数/参照メンバを持つ要素を使用するstd::vectorなどのコンテナは、ほとんどの場合すぐに未定義の動作になってしまう。例えば
struct X {
    const int i;
    X(int _i) : i(_i) {}
    friend std::ostream& operator<< (std::ostream& os, const X& x) {
        return os << x.i;
    }
};
std::vector<X> v;
v.push_back(X(42));
v.clear();
v.push_back(X(77));
std::cout << v[0]; // 未定義の動作
その理由はvector::push_back(他の挿入関数と同じ)は、配置 new の戻り値を無視するアロケータのconstruct関数を介して配置 new を呼び出しているからである。3.8 Object lifetime [basic.life]§8 に従って、値を保持する内部的に使用されるオブジェクトが新しい値を参照するという保証はない。具体的には、std::vectorは次のようになる(ムーブセマンティクスを使用しないなど、いくつかの単純化を行なっている)。
template <typename T, typename A = std::allocator<T>>
class vector
{ 
public:
    typedef typename std::allocator_traits<A> ATR;
    typedef typename ATR::pointer pointer;
private:
    A _alloc; // 現在のアロケータ
    pointer _elems; // 要素の配列
    size_t _size; // 要素のサイズ
    size_t _capa; // 容量
public:
    void push_back(const T& t) {
        if (_capa == _size) {
            reserve((_capa+1)*2);
        }
        ATR::construct(_alloc, _elems+_size, t); // 配置 new を呼び出す
        ++_size;
    }
    T& operator[] (size_t i){
        return _elems[i]; // 置き換えられた要素のメンバーが定数であるため未定義の動作
    }
 …
}; 
再度、ATR::constructは、呼び出された配置 new の戻り値を返さないことに留意。従って、_elemsの代わりにこの戻り値を使用することはできない。再びこの問題を解決するためにstd::launderが提案されたが、以下で説明するように、この問題はstd::launderでは解決できない。
C++11 以前では定数メンバを持つ要素を使用することはできなかった(vector)か、正式にはサポートされなかった。何故なら、要素はCopyConstructible と Assignable でなければならなかったからである(リストなどのノードベースのコンテナはconstメンバー)。しかし、C++11 では、ムーブセマンティクスを導入することで、上のクラスXとして定数メンバを持つ要素がサポートされたため、この未定義の動作が発生する。

How launder() tries to solve the problem

現在の標準によると、次のコードは未定義の動作となる。CWGはstd::launderを次のように導入することで問題を解決すること決めた: プログラムがこの問題が発生する可能性のあるデータに影響を与えるたびに、std::launderを使用してデータにアクセスする必要がある:
struct X {
 const int n;
};
X* p = new X{7};
new (p) X{42}; // pに新しい値を置くことを要求する
int b = p->n; // 未定義の動作
int c = std::launder(p)->n; // OK, c は 42 (launder(p) は double-check を強制する)
int d = p->n; // まだ未定義の動作
std::launderはその利用のためにポインタを「白く洗い流す」ことに留意。最後の行が示しているように、配置 new が呼び出されたデータにアクセスするときは常にstd::launderを使用する必要がある。つまり、現在(技術的に)壊れているコードを修正し、定数/参照要素を持つ replacement メモリを使用する場合は、std::launderを使用してこのコードを修正する必要がある。しかし、若干異なる解決策があるかもしれない。例えば Richard Smith 氏が指摘したように
もう1つの選択肢は、要素が挿入または削除されたときにのみlaunderを行うことである。
(_M_begin = std::launder(_M_begin)) // あなたのコンパイラが十分にスマートであるという前提の下で
コード生成時にこれらのストアを削除する。
しかし、いずれにしても、これらすべてのラッパー/コンテナクラスの既存のコードで何かを修正する必要がある。

Why launder() doesn’t work for allocator‐based containers

std::vectorなどの全てのアロケータベースのコンテナでは、未定義の動作を避けるためのstd::launderは問題となる。例えば、上記の単純化されたベクトルの例でstd::launderを使用して、置き換え可能なオブジェクトのデータに定数要素を使用してアクセスすることを考える:
template <typename T, typename A = std::allocator<T>>
class vector
{
public:
    typedef typename std::allocator_traits<A> ATR;
    typedef typename ATR::pointer pointer;
private:
    A _alloc; // 現在のアロケータ
    pointer _elems; // 要素の配列
    size_t _size; // 要素のサイズ
    size_t _capa; // 容量
 …
};
演算子`[]`の未定義の動作を修正するためには、置き換えられたメモリを参照するポインタにstd::launderを適用する必要がある。:
  • 明白な解決策は次のコードかもしれない:
    T& operator[] (size_t i){
        return std::launder(_elems)[i]; // まだUBかもしれない
    } 
    
    残念ながら_elemsの型は生のポインタ型ではない可能性があるため、これは未定義の振る舞いにつながる可能性がある。 Jonathan Wakely が指摘しているように、std::allocator_traits::pointerがクラス型である可能性がある:
    std::launderは引数として生ポインタを取るが、アロケータの "ポインタ"型が生ポインタでない場合、それをlaunderに渡すことはできない。
  • もう一つの生のポインタがある。しかし:
    T& operator[] (size_t i){
        return std::launder(this)->_elems[i]; // launder() は効果がない
    }
    
    Richard Smith 氏が指摘したように、std::launder(this)のタイプはちょうどこれと同等である:
    寿命が終わったオブジェクトを指し、新しいオブジェクトが同じストレージに作成されたオブジェクトをpが指していない限り、launder(p)は効果がない事に留意されたし。
つまり、一般的にはstd::launderによって希望の効果が得られるような生のポインタがないのである。従って、std::launderは、定数/参照メンバを持つ要素を持つときに、アロケータベースのコンテナで未定義の動作を回避するために問題を解決しないのである。

Why not fixing the Basic Lifetime Guarantees Instead?

直接的な疑問は、何故単純にメモリモデルを修正して配置newが行われるときに暗黙的に launder が行われるようにしないのかである。再び注意:この問題は新しいものではない。関連する wording は C++98 の一部ではないが、C++03(その時点では3.8§7)の一部である。よって、長い間、この問題は少なくとも正式に存在していると主張する事ができる。
もう一つの疑問は、既存の習慣に関する事である。
つまり、コンパイラがこの種の最適化を使用するかどうかによって異なる:
定数/参照の最適化に関して、現在のコンパイラは通常:
  • 私が知っている限り、gcc は現在、定数/参照の場合最適化していないが、 vptr 部分の最適化は行っている
  • 私が知っている限り、clang は現在いくつかのconst関連の最適化に取り組んでおり、これらの問題が有効になる可能性がある。
  • XLコンパイラに関しては、Hubert Tongが以下のように示している:
    XLコンパイラは、定数/参照の場合にも最適化する。しかし、これは、静的記憶領域の場合に限定されることを意味し、例えば
    struct A {
        const int x;
        int y;
    };
    
    A a = { 42, 0 }; 
    
    void foo(); // 定義は見えない
     
    int main(void) {
        foo();
        return a.x;
    } 
    
    a.x の読み込みは 42 に置き換えられる。これは2004年以前のケースである。
このようにコアルールによって定められたことによる結果を見ることができる。(実際には、多くのコードが壊れてしまうことが予想されるので、実際には、遅かれ早かれ後戻りするかもしれないが、この不必要な努力と混乱を避けるべきである。)

Is there more than the constant/reference member issue?

別の質問は、定数/参照の問題のほかに同様の問題があるかどうかである。実際、Richard Smith 氏はプライベート・Eメールで述べている。
しかし、"dynamic type" の規定は極めて価値があることが知られている。特に、
X *p; // pは dynamic type が Y であるオブジェクトを指す事が知られている
p->virtual_f(); // devirtualize する事ができる
p->virtual_g(); // これも devirtualize する事ができる
二回目の呼び出しを devirtualize する事ができるためパフォーマンスが大幅に向上し、それは最初の関数が配置newを使用してオブジェクトの dynamic type を変更しなかったと仮定することが許可されているためにのみ可能である。
別の例:
struct B { virtual void f(); };
struct D1 : B { virtual void f(); };
struct D2 : B { virtual void f(); };
variant<D1, D2> v;
ここでvariantは、アクティブな要素をD1D2の間で変更できるようにするために、内部的にlaunder のようなものを使用し、正しい関数を確実に呼び出すための仮想呼び出しを必要とする(これは本質的に上記p->virtual_f() / p -> virtual_g()上記の場合、場合と同じである。)。
私が知っている限りでは、これらのケースはコンテナやラッパータイプを使用しているときに起こることはないか、ごく稀である。よって、std::launderが効果を発揮しない const / reference のケースを修正しながら、これらのケースに対してstd::launderを提供することそのものには価値がある。

Hiding the problem could make things worse

この問題は、対応するメンバ関数を呼び出すことによって間接的に発生する可能性があることにも留意されたし:
Obj x;
x.use(); // ok
mutate(x); // 配置 new の呼び出し?
x.use(); // 未定義の動作かもしれない
std::launder(&x)->use(); // 必要かもしれない
x.use(); // まだ未定義の動作かもしれない
mutate()が 配置new を呼び出さないことを知っていない限り、オブジェクトxを使用するたびに必ずstd::launder()を使用する必要があるが、ここでもstd::launderを適用するための未処理のポインタはない(ここでもstd::launder(&x)はxが生きているので&xと同じである)。
Nathan Myers 氏は以下のように薦める。
私たちが本当に望むのは、名前の特定の使用(例えば argument)がそれを証明することを宣言する事である。これは実際に厄介な作業を行うメンバ関数を含む関数の特定の引数に付随する事ができる。例えば:
template <T> void default_construct(T taint* p) { new(p) T(); } 
恐らく、コンパイラはそのように宣言されていない引数がそれに渡された場合、警告する必要がある。std::vector::push_backの場合、1つの vector メンバーのみが汚染*2される。そのメンバーは、宣言されたメンバー機能による汚損の対象として最もよく識別される。つまり、汚れたメンバ関数を呼び出すだけでは、このすべてが汚されることはないが、汚れたメンバーvec::ptrだけはやはり汚れてしまう。いずれにしても、rvalue 自体は、必要な各場所でコンパイラによって目に見えないほど laundered される。コンパイラプログラマよりもはるかに優れている。vector<>::data()vector<>::push_back()の場合は、前者の結果と後者の呼び出しの間に同様の関係が必要なようである。
そのような機能を追加する期間は短いの AFNOR の変更によって実装に拡張された緯度を98→03に制限することは、恐らくC++14(及びC++ 11)の corrigendum の中で必要と思われる。
別の中間的な手段は、上記の例の3番目の "o.use()"がOKになるように、launderation を固着させる(別名 whitewashing)ようにする事である。
Obj o;
o.mod(); // o tainted
launder(&o);
o.use(); // ok
しかし、これを corrigendum に入れるのは難しいだろう。何にせよ、これは C++17 のために、必要だと思われる。

Summary and Recommendation

ここで説明する問題は、20年以上にわたって利用可能な2つのプログラミング手法を使用している。
  • 常に配置newを返却値として使わない
    • 便利のため
    • 追加のオブジェクトを導入することを避けるため(variantまたはoptionのように)
    • アロケータを使用するすべてのクラスに他のオプションがないため
  • 定数メンバーを持つクラス
  • アロケータベースのコンテナ
全ての組み合わせが常に可能ではないが(例えば、vectorC++11 まで定数メンバを持つ要素を使用できなかった)、一定のメンバーを持つオブジェクトとの組み合わせで配置 new を使用するコードが多くある。(C++11より前は、例えばノードベースのコンテナで簡単に起こる事があった。)。コンパイラが現在対応する最適化をオンにしていれば、既存のコードの多くが暗黙的に壊れてしまうと思われる。従って、現在の実装では、このような変更がキャンセルされる可能性がある。しかし私はこの既存のコードを有効にするための wording を明確にすべきであると強く提案する。標準は常に既存の慣行を標準化すべきである。しかし、これらの最適化の1つがもはや利用できない場合、どのくらいのコードが壊れているのかは分からない。定数/参照メンバの部分については、このような最適化がまだ広く普及していないように思われるため、壊れたコードは然程ないのではないかと思われる。実際に、既存のすべての汎用ラッパークラスとコンテナクラスがそれに応じて実装されることは期待できない(問題は標準ライブラリクラスにのみ適用されるものではないのである)。また、指摘したように、std::launderを使用した現在のソリューションでは、const / reference メンバーを持つ要素を持つアロケータベースのコンテナを使用した未定義の動作の問題は解決されない。
よって私が見た唯一の妥当なオプションは、[basic.life]の C++98 バージョンに戻って、const / referenceメンバのないクラスに対してのみ、既に許可している最適化を許可する事である。

*1:訳注: n3690 3.8 [basic.life] paragraph 7std::optional のような再配置を必要とするモデルの実装を困難にするとしてこの議論が取り上げられた:https://groups.google.com/a/isocpp.org/forum/#!msg/std-proposals/93ebFsxCjvQ/myxPG6o_9pkJ

*2:本文では "tainted" とある。恐らく launder の対義語として述べているのだと思われる