yohhoyの日記

技術的メモをしていきたい日記

オーバロード解決の優先順位制御

プログラミング言語C++における関数テンプレートのオーバーロードにおいて、SFINAEと組み合わせてオーバーロード解決の優先順を制御するテクニック。

選択候補が2個のケース

2つの型T, Uに対して、1) 演算 T / U が定義されていれば同演算子を、2) そうでなければ T * (1/U) を選択するケースを考える。*1

// 実装関数 第1候補
template <typename T, typename U>
auto f_impl(T a, U b, int) -> decltype(a / b)
{ return a / b; }

// 実装関数 第2候補
template <typename T, typename U>
auto f_impl(T a, U b, char) -> decltype(a * (U{1} / b))
{ return a * (U{1} / b); }

// 公開関数
template <typename T, typename U>
auto f(T a, U b)
{ return f_impl(a, b, 0); }

実装関数テンプレートの第3引数に 1) int型、2) char型 を指定し、f_impl呼び出し側で 0 指定により優先順制御を行う。関数のオーバーロード解決(overload resolution)において、リテラル 0 がint型として扱われる規則が、char型として扱われる規則より優先されるため。*2

選択候補がN個のケース

優先順位付けを表現するrank<I>ヘルパクラスを導入する。公開関数で指定するrank<2>実引数に対して、オーバーロード解決時には基底クラスrank<1>よりもrank<2>へと優先的にマッチするため。同様にrank<0>よりもrank<1>が優先する。

template <int I> struct rank : rank<I-1> {};
template <> struct rank<0> {};

// 実装関数 第1候補
template <typename T, typename U>
auto f_impl(T a, U b, rank<2>) -> decltype(a / b)
{ return a / b; }

// 実装関数 第2候補
template <typename T, typename U>
auto f_impl(T a, U b, rank<1>) -> decltype(a * (U(1) / b))
{ return a * (U(1) / b); }

// 実装関数 第3候補
template <typename T, typename U>
int f_impl(T a, U b, rank<0>)
{ return 0; }

// 公開関数
template <typename T, typename U>
auto f(T a, U b)
{ return f_impl(a, b, rank<2>{}); }

メモ:Web上で本テクニックを利用するコードをちらほら見かける。名前の付いたIdiomではないのだろうか?

関連URL

*1:ここでは公開関数f()でのみ通常関数での戻り値型推論を利用している。実装関数f_implの末尾戻り値型宣言を省略するとSFINAEが機能せずに、式 T / U の有効有無にかかわらず1)にオーバーロード解決されてしまう。

*2:リテラル0はC++14 2.14.2/p2よりint型と解釈される。優先順位は13.3.3.1.1のstandard conversion sequenceにある1) No conversions required→Eact Match Rankと2) Integral conversions→Conversion Rankに基づく…ような気がする。オーバーロード解決規則は難解すぎて理解できない。

Customization Point Object

C++2a(C++20)標準ライブラリに導入される Customization Point Object についてメモ。*1

まとめ:

  • Customization Point == ユーザ定義型に対して事前定義した動作カスタマイズ可能点。具体的な処理実装ソースコードから呼び出される名前。
  • Customization Point Object(CPO) == 上記目的のためにライブラリ側で定義する、グローバルな関数オブジェクト。
  • 最初に「コンセプトによる型制約を検査」してから「ADLによって適切な関数オーバーロードを検索」するための仕組み。
    • C++17現在の関数オーバーロード方式では、[A]誤って利用されるリスクが高く、[B]コンセプトを用いた型制約を強制できない という問題がある。
  • CPO呼び出しの引数型は、CPOが要求するコンセプトを満たす。引数型リストがコンセプトを満たさない場合は、オーバーロード解決候補から除外される。
  • CPO呼び出しの戻り値型は、CPOに対して定めたコンセプトを満たす。*2

C++17

C++17標準ライブラリでは swap, begin, end 関数がCustomization Pointとなっている*3。ユーザ定義型に対するカスタマイズが正しく機能するには、“名前空間std配下の名前swapを導入(using)”した後に、“非修飾名(unqualified)でswap関数を呼び出す”必要がある。この挙動の理解には込み入った知識を要求するため、C++プログラマに誤使用されやすいという問題があった。

namespace NS {
  struct S;
  // ユーザ定義型に対するカスタム動作
  void swap(S&, S&);
}

// OK: 正しいswap呼び出し
template <typename T>
void func0(T& a, T& b) {
  using std::swap;
  swap(a, b);
  // 変数型TがNS名前空間に属する場合、ADLにより
  // カスタマイズ版(NS::swap)関数が呼び出される
}

// NG: 完全修飾名による呼び出し
template <typename T>
void func1(T& a, T& b) {
  std::swap(a, b);
  // T=NS::Sに対してカスタマイズ版が呼び出されない
}

さらにC++2aではコンセプトを用いたテンプレートパラメータ制約を表現可能となるが、ユーザ定義型に対してライブラリ側で事前設計した型制約を回避できてしまうという問題が生じる。

namespace std {
  // 仮想的なコンセプト: Fooable
  template <typename T> concept Fooable = /*...*/;
  // 仮想的なCustomization Point: T型はFooableを満たすべき
  template <Fooable T> void foo(T x) { /*...*/ }
}

namespace NS {
  // std::Fooableコンセプトを満たさないユーザ定義型
  struct S;
  // ユーザ定義型に対するカスタム動作(型制約を無視)
  void foo(S);
}

NS::S x;
using std::foo;
foo(x);  // ★OK!?
// std::Fooableコンセプト要件の検査をバイパスして
// カスタマイズ版NS::fooの呼び出しに成功してしまう

C++2a

C++標準ライブラリへのCPO導入によって、既存の2つの課題解決をはかっている。(説明用Customization Pointとして名前 foo を利用)

  • 完全修飾名呼び出し std::foo(a); または非修飾名呼び出し using std::foo; foo(a); は、いずれの呼び出しでも同じ振舞いになること。
  • using std::foo; foo(a);としても、CPO foo が要求する型制約がバイパス(無視)されないこと。

CPOの実装例は次の通り。

namespace std {
  // 仮想的なコンセプト: Fooable
  template <typename T> concept Fooable = /*...*/;
  // 少なくともint型, double型はFooableコンセプトを満たすと仮定

  namespace detail {
    // ライブラリ標準のfoo動作を定義...
    void foo(int T);     // #1
    void foo(double T);  // #2

    struct foo_cpo {
      // 型パラメータTはFooableコンセプトを満たすべき
      template <Fooable T>
      void operator()(T a) {
        // 非修飾名呼び出しによってdetail::foo()への解決
        // またはADLによる名前解決が行われる
        foo(a);
      }
    };
  }

  // グローバル関数オブジェクトとしてCPO定義
  inline constexpr detail::foo_cpo foo{}; 
}

namespace NS {
  // std::Fooableコンセプトを満たさないユーザ定義型NS::BadS
  struct BadS;
  void foo(BadS);  // #3
  // std::Fooableコンセプトを満たすユーザ定義型NS::FooU
  struct FooU;
  void foo(FooU);  // #4
}

int val = 42;
NS::BadS bad;
NS::FooU good;
// 完全修飾名による呼び出し
{
  std::foo(val);  // OK: #1を呼び出し
  std::foo(bad);  // NG: ill-formed
  std::foo(good); // OK: #4を呼び出し
}
// using+非修飾名による呼び出し
{
  using std::foo;
  foo(val);  // OK: #1を呼び出し
  foo(bad);  // NG: ill-formed
  foo(good); // OK: #4を呼び出し
}

WD N4810現在のC++2a標準ライブラリでは、Rangesライブラリ要素として下記CPOを導入する(名前空間stdは省略)。後方互換性維持のため、従来 swap, begin, endC++17ライブラリ仕様のまま維持される。

  • ranges::swap
  • ranges::iter_move, ranges::iter_swap
  • ranges::(c)(r)begin, ranges::(c)(r)end*4
  • size
  • empty
  • data, cdata
  • view::single
  • view::iota
  • view::all
  • view::filter
  • view::transform
  • view::take
  • view::join
  • view::split
  • view::counted
  • view::common
  • view::reverse

関連URL

*1:この記事はC++20を相談しながら調べる会 #1のアウトプットとして書かれました。参加中に全てをまとめた訳ではないのですが、同イベントは強い動機付けになっています。

*2:ライブラリでは名前 foo とコンセプトCを定義しておき、ライブラリ内部実装がコンセプトCを要求する箇所において式 foo(x) を利用する。

*3:C++17言語仕様には直接"Customization Point"という用語は登場しないが、swap, begin, end 関数テンプレートを非修飾名で呼び出すことを規定している。このようなC++ライブラリ仕様により、ユーザ定義型に対するカスタマイズ版が適切に利用される。

*4:begin, cbegin, rbegin, crbeginの略。endについても同様。

swap(T, U)とis_swappable_with<T, U>とvector<bool>

C++17標準ライブラリには「型が異なる変数間での値交換(swap)」可能か否かを判定するメタ関数 std::is_(nothrow_)swappable_with<T, U> が存在する。一般的には値交換操作は同一型変数間(swap(T&, T&))で行われるが、プロキシ型(proxy)のような特殊ケースにおいて異型変数間での値交換(swap(T, U))が必要となるため。*1

// <type_traits>ヘッダ
namespace std {
  template <class T, class U>
  struct is_swappable_with;

  template <class T, class U>
  struct is_nothrow_swappable_with;
}

C++17 20.5.3.2/p5 Exampleを一部転用したコード例:

#include <type_traits>
#include <utility>

namespace N {
  struct A { int m; };
  struct Proxy { A* a; };
  Proxy proxy(A& a) { return Proxy{ &a }; }
  void swap(A& x, Proxy p) {
    std::swap(x.m, p.a->m);
  }
  void swap(Proxy p, A& x) { swap(x, p); }
}

N::A a1 = { 1 }, a2 = { 2 };
auto p2 = N::proxy(a2);

// N::A& と N::Proxy 間で値交換可能
static_assert(std::is_swappable_with_v<N::A&, N::Proxy>);

swap(a1, p2);  // OK
assert(a1.m == 2 && a2.m == 1);

std::vector<bool>コンテナ

(一部で悪名高い)std::vector<bool> コンテナクラスはこのようなプロキシ型を利用する。同コンテナはbool値のビット単位管理によりメモリを効率的に利用できるが*2、その代償としてbool型要素への参照bool&を直接返せないため、要素への添字アクセスv[0]などはプロキシ型vector<bool>::referenceを返す実装となっている。

#include <vector>
#include <utility>

std::vector<bool> v{ true };
bool b = false;

static_assert(
  std::is_swappable_with_v<std::vector<bool>::reference, bool&>
);  // OK?

swap(v[0], b);  // OK?

上記コードでは vector<bool>プロキシ型 と bool型変数 が値交換可能であることを期待しているが、C++17現在の標準ライブラリ仕様では該当コードの動作を保証しないGCC/libstdc++*3、Clang/libc++*4ではコンパイル&実行可能だが、MSVC 19.16ではコンパイルエラーとなる。*5

この問題は P0022R2, 3.1 Proxy Iterator problemsにて言及されている。

For all its problems, vector<bool> works surprisingly well in practice, despite the fact that fairly trivial code such as below is not portable.

std::vector<bool> v{true, false, true};
auto i = v.begin();
bool b = false;
using std::swap;
swap(*i, b);  // Not guaranteed to work.

Because of the fact that this code is underspecified, it is impossible to say with certainty which algorithms work with vector<bool>. That fact that many do is due largely to the efforts of implementors and to the fact that bool is a trivial, copyable type that hides many of the nastier problems with proxy references. For more interesting proxy reference types, the problems are impossible to hide.

関連URL

*1:本記事はnakameguro_feature.cpp vol.17勉強会で取り上げられた疑問がきっかけ。

*2:一般的なC++処理系では、1バイト中に8個のbool値を詰め込むことで消費メモリサイズを節約できる。

*3:https://github.com/gcc-mirror/gcc/commit/5345c53733c161a7781dd55559a4e1458751da1d

*4:https://github.com/llvm-mirror/libcxx/blob/bc8d3f97eb5c958007f2713238472e0c1c8fe02c/include/__bit_reference#L75-L93

*5:https://gcc.godbolt.org/z/Ts_vLF

Goodbye "bit" in C++, (Partially)

C++2a(C++20)言語仕様の定義においては、用語 "bit" の利用はできるだけ回避される(完全に無くなる訳ではない)。これはC++2a言語仕様変更「符号付き整数型==2の補数表現を保証」の影響。

提案文書 P1236R1 Alternative Wording for P0907R4 Signed Integers are Two's Complement 冒頭部より引用。

This paper presents alternative wording for P0907R3 Signed Integers are Two's Complement by Jean François Bastien, avoiding talking about unobservable bits as much as possible.

http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2018/p1236r1.html

整数型サイズの表現では、独自の用語 "range exponent" が利用される。N4800(C++2a WD) 6.7.1/p4より一部引用。

Table 10 -- Minimum range exponent

Type Minimum range exponent N
signed char 8
short 16
int 16
long 32
long long 64

The range exponent of each signed integer type shall not be less than the values specified in Table 10. The value representation of a signed or unsigned integer type comprises N bits, where N is the respective range exponent. Each set of values for any padding bits (6.7) in the object representation are alternative representations of the value specified by the value representation. (snip)

関連URL

ラムダキャプチャ中での前置ellipsisと後置ellipsis

プログラミング言語C++において可変長引数テンプレートパラメータパックをラムダ式でキャプチャする際、そのキャプチャ方式によってellipsis(...)の前置/後置が異なることに注意。

template <typename... Ts>
void func(Ts... xs)
{
  // C++11以降: 簡易キャプチャ(simple-capture)
  [xs...] {
    // xsの各要素をコピーキャプチャ
  };

  [&xs...] {
    // xsの各要素を参照キャプチャ
  };

  // C++2a: 初期化キャプチャ(init-capture)
  [...xs = std::move(xs)] {
    // xsの各要素をムーブキャプチャ
  };
}

N4800(C++2a WD) 7.5.5.2 構文規則, p17より一部引用。

lambda-capture :
  capture-list
  (snip)
capture-list :
  capture
  capture-list , capture
capture :
  simple-capture ...opt
  ...opt init-capture

A simple-capture followed by an ellipsis is a pack expansion (12.6.3). An init-capture preceded by an ellipsis is a pack expansion that introduces an init-capture pack (12.6.3) whose declarative region is the lambda-expression's compound-statement. [Example:

template<class... Args>
void f(Args... args) {
  auto lm = [&, args...] { return g(args...); };
  lm();

  auto lm2 = [...xs=std::move(args)] { return g(xs...); };
  lm2();
}

-- end example]

メモ:P0780R1時点では初期化キャプチャ(init-capture)も後置ellipsis方式だったが、最終的に前置ellipsisに変更されてC++2a言語仕様に採択された。“導入される名前直前にellipsisを記述”という既存構文との一貫性を重視したのこと。*1

関連URL

*1:P0780R2: "Following Core and Evolution guidance, the ellipses for an init-capture pack have been moved from following the init-capture to preceding it. This is consistent with the existing practice of ... preceding the name that it introduces."

SecureStringクラスは非推奨

.NET Frameworkで提供される System.Security.SecureStringクラス は、新規コードでは利用しないこと。

Important

We don't recommend that you use the SecureString class for new development. For more information, see SecureString shouldn't be used on GitHub.

https://docs.microsoft.com/en-us/dotnet/api/system.security.securestring

Recommendation

Don't use SecureString for new code. When porting code to .NET Core, consider that the contents of the array are not encrypted in memory.
The general approach of dealing with credentials is to avoid them and instead rely on other means to authenticate, such as certificates or Windows authentication.

DE0001 SecureString shouldn't be used

関連URL