yohhoyの日記

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

式のコンパイル時評価判定

C++20言語機能を利用した、ある式がコンパイル時に評価可能かを判定するメタ関数的なもの。id:yohhoy:20190528 の別解。

本記事の内容はStackOverflowで見つけた質問と回答に基づく。

// C++20以降
template<class Lambda, int = (Lambda{}(), 0)>
constexpr bool is_constexpr(Lambda) { return true; }
constexpr bool is_constexpr(...) { return false; }

template<int N> void do_stuff();
void do_stuff(int N);

template<typename T>
int process(const T&)
{
  // 式 T::number() をコンパイル時に評価可能?
  if constexpr (is_constexpr([]{ T::number(); })) {
    do_stuff< T::number() >();
    return 1;
  } else {
    do_stuff( T::number() );
    return 2;
  }
}

struct X { static constexpr int number() { return 42; } };
struct Y { static int number() { return 42; } };
int main()
{
  assert(process(X{}) == 1);  // OK(C++17: NG)
  assert(process(Y{}) == 2);  // OK
}

関連URL

関数名/メンバ関数名とアドレス演算子

プログラミング言語C++において、通常関数名やstaticメンバ関数名から関数ポインタ型へは暗黙変換が行われるが、メンバ関数名からメンバ関数ポインタ型への変換はアドレス演算子&利用が必須。

void f0();
struct S {
  static void f1();
  void mf();
};

// (通常)関数/staticメンバ関数
using PF = void (*)();
PF p0a = &f0;  // OK
PF p0b =  f0;  // OK: 暗黙変換
PF p1a = &S::f1;  // OK
PF p1b =  S::f1;  // OK: 暗黙変換

// メンバ関数
using PMF = void (S::*)();
PMF pmfa = &S::mf;  // OK
PMF pmfb =  S::mf;  // NG: ill-formed

C++03 4.3/p1, 5.1/p10より一部引用。C++20現在は7.3.4/p1, 7.5.4.1/p2が対応。

An lvalue of function type T can be converted to an rvalue of type "pointer to T." The result is a pointer to the function.50)

脚注50) This conversion never applies to nonstatic member functions because an lvalue that refers to a nonstatic member function cannot be obtained.

An id-expression that denotes a nonstatic data member or nonstatic member function of a class can only be used:

  • (snip)
  • to form a pointer to member (5.3.1), or
  • (snip)

関連URL

std::destructibleコンセプト

C++20標準ライブラリstd::destructible<T>コンセプトに関するメモ。

ある型Tが「例外送出なしにデストラクト可能」と制約(constraint)するコンセプト。デストラクタは既定で暗黙のnothrow指定が行われるため*1、明示的にnothrow(false)指定を行わない限りあらゆるオブジェクト型が満たすコンセプトとなる。*2

std::destructibleコンセプトは、標準ヘッダ <concepts> 提供のオブジェクト関連コンセプトにより直接/間接的に包摂(subsume)される。

  • std::constructible_from
  • std::default_initializable
  • std::move_constructible
  • std::copy_constructible
  • std::movable
  • std::copyable
  • std::semiregular
  • std::regular

C++20 18.4.10, 18.4.11より引用(下線部は強調)。

1 The destructible concept specifies properties of all types, instances of which can be destroyed at the end of their lifetime, or reference types.

template<class T>
  concept destructible = is_nothrow_destructible_v<T>;

2 [Note: Unlike the Cpp17Destructible requirements (Table 32), this concept forbids destructors that are potentially throwing, even if a particular invocation of the destructor does not actually throw. -- end note]

1 The constructible_from concept constrains the initialization of a variable of a given type with a particular set of argument types.

template<class T, class... Args>
  concept constructible_from = destructible<T> && is_constructible_v<T, Args...>;

2016年頃のC++ Ranges拡張PDTS*3時点でも、Destructibleコンセプト*4はオブジェクトコンセプトにおける最も基本的なコンセプトと説明されていた。(PDF)N4622より一部引用(下線部は強調)。

19.4 Object concepts
1 This section describes concepts that specify the basis of the value-oriented programming style on which the library is based.

19.4.1 Concept Destructible
1 The Destructible concept is the base of the hierarchy of object concepts. It specifies properties that all such object types have in common.

template <class T>
concept bool Destructible() {
  return requires (T t, const T ct, T* p) {
    { t.~T() } noexcept;
    /*(snip)*/
  };
}

(snip)

19.4.2 Concept Constructible
1 The Constructible concept is used to constrain the type of a variable to be either an object type constructible from a given set of argument types, or a reference type that can be bound to those arguments.

template <class T, class ...Args>
concept bool __ConstructibleObject =
  Destructible<T>() && requires (Args&& ...args) { /*(snip)*/ };

template <class T, class ...Args>
concept bool Constructible() {
  return __ConstructibleObject<T, Args...> || /*(snip)*/
}

関連URL

*1:C++20 14.5/p8: "The exception specification for an implicitly-declared destructor, or a destructor without a noexcept-specifier, is potentially-throwing if and only if any of the destructors for any of its potentially constructed subobjects is potentially-throwing or the destructor is virtual and the destructor of any virtual base class is potentially-throwing."

*2:参照型(reference type)はオブジェクト型(object type)ではないが(C++20 6.8.1/p8)、std::is_nothrow_destructible メタ関数の定義より std::destructible コンセプトを満たす。(cv修飾された)void 型/関数型/要素数未定の配列型は、std::destructible コンセプトを満たさない。

*3:Preliminary Draft Technical Specification

*4:検討段階におけるコンセプト関連の言語仕様は、C++20現在のコンセプト仕様とは異なっている。またコンセプト命名規則はその後の(PDF)P1754R1採択により Destructible→destructible/Constructible→constructible_from へと変更されている。

std::expectedと強い例外安全性

C++2b(C++23)標準ライブラリに追加されるstd::expected<T, E>の例外安全性についてメモ。

正常型T/エラー型Eを値(value)として取り扱いつつ「強い例外安全性(Strong exception safety)」を満たすために、テンプレートパラメータに一定の制約が課される。

  • ムーブ/コピー代入:TもしくはEのいずれかが “nothrowムーブ構築可能” であること
  • 正常型U代入:Tが “nothrow構築可能” または、TもしくはEのいずれかが “nothrowムーブ構築可能” であること
  • エラー型G代入:Eが “nothrow構築可能” または、TもしくはEのいずれかが “nothrowムーブ構築可能” であること
  • swapTもしくはEのいずれかが “nothrowムーブ構築可能” であること

T型/E型の少なくとも一方でnothrow保証のある構築手段を確保し、他方の型において例外送出された場合は操作前状態までロールバック処理*1を行う(後述引用のcatch節を参照)。

C++2b DIS N4950 22.8.6.4/p1-2, 22.8.6.5/p2より引用。各種代入演算子(operator=)の効果は説明用reinit-expected関数を用いて規定される。

1 This subclause makes use of the following exposition-only function:

template<class T, class U, class... Args>
constexpr void reinit-expected(T& newval, U& oldval, Args&&... args) {  // exposition only
  if constexpr (is_nothrow_constructible_v<T, Args...>) {
    destroy_at(addressof(oldval));
    construct_at(addressof(newval), std::forward<Args>(args)...);
  } else if constexpr (is_nothrow_move_constructible_v<T>) {
    T tmp(std::forward<Args>(args)...);
    destroy_at(addressof(oldval));
    construct_at(addressof(newval), std::move(tmp));
  } else {
    U tmp(std::move(oldval));
    destroy_at(addressof(oldval));
    try {
      construct_at(addressof(newval), std::forward<Args>(args)...);
    } catch (...) {
      construct_at(addressof(oldval), std::move(tmp));
      throw;
    }
  }
}
constexpr expected& operator=(const expected& rhs);

2 Effects:

  • If this->has_value() && rhs.has_value() is true, equivalent to val = *rhs.
  • Otherwise, if this->has_value() is true, equivalent to: reinit-expected(unex, val, rhs.error())
  • Otherwise, if rhs.has_value() is true, equivalent to: reinit-expected(val, unex, *rhs)
  • Otherwise, equivalent to unex = rhs.error().

Then, if no exception was thrown, equivalent to: has_val = rhs.has_value(); return *this;

constexpr void swap(expected& rhs) noexcept(see below);

2 Effects: See Table 63.

Table 63: swap(expected&) effects

this->has_value() !this->has_value()
rhs.has_value() equivalent to: using std::swap; swap(val, rhs.val); calls rhs.swap(*this)
!rhs.has_value() see below equivalent to: using std::swap; swap(unex, rhs.unex);

For the case where rhs.value() is false and this->has_value() is true, equivalent to:

if constexpr (is_nothrow_move_constructible_v<E>) {
  E tmp(std::move(rhs.unex));
  destroy_at(addressof(rhs.unex));
  try {
    construct_at(addressof(rhs.val), std::move(val));
    destroy_at(addressof(val));
    construct_at(addressof(unex), std::move(tmp));
  } catch(...) {
    construct_at(addressof(rhs.unex), std::move(tmp));
    throw;
  }
} else {
  T tmp(std::move(val));
  destroy_at(addressof(val));
  try {
    construct_at(addressof(unex), std::move(rhs.unex));
    destroy_at(addressof(rhs.unex));
    construct_at(addressof(rhs.val), std::move(tmp));
  } catch (...) {
    construct_at(addressof(val), std::move(tmp));
    throw;
  }
}
has_val = false;
rhs.has_val = true;

関連URL

AutoClosable#closeメソッドのべき等要件

プログラミング言語Javaのtry-with-resources構文で用いるAutoClosableインタフェース仕様では、closeメソッドのべき等(idempotent)性は要求されない。(実装時には"べき等"が強く推奨される)

void close()
    throws Exception

Closes this resource, relinquishing any underlying resources. This method is invoked automatically on objects managed by the try-with-resources statement.
(snip)
Note that unlike the close method of Closeable, this close method is not required to be idempotent. In other words, calling this close method more than once may have some visible side effect, unlike Closeable.close which is required to have no effect if called more than once. However, implementers of this interface are strongly encouraged to make their close methods idempotent.

https://docs.oracle.com/javase/8/docs/api/java/lang/AutoCloseable.html#close--
void close()
    throws IOException

Closes this stream and releases any system resources associated with it. If the stream is already closed then invoking this method has no effect.

https://docs.oracle.com/javase/8/docs/api/java/io/Closeable.html#close--

関連URL

空っぽの構造体

メンバ変数を1個ももたない空(empty)の構造体は、C++言語ではwell-definedとされるが、C言語ではill-formedとなる。

// C++: OK / C: NG
struct S { };

GCCでは独自拡張として空の構造体を許容するが、標準C++とは異なり構造体サイズが0となることに注意。

// GNU C拡張
struct S {};
printf("%zu", sizeof(struct S));  // 0
// 標準C++
struct S {};
printf("%zu", sizeof(struct S));  // 1

std::mdspan×空間充填曲線

C++2b(C++23)標準ライブラリの多次元ビューstd::mdspanクラステンプレート(→id:yohhoy:20230303)は、第3テンプレートパラメータLayoutPolicyを介して任意のメモリレイアウトをサポートする。*1

C++2b標準ライブラリは、多次元配列表現としてメジャーな行優先(row major)/列優先(column major)レイアウトstd::layout_rightstd::layout_left、これらを汎化したストライドレイアウトstd::layout_strideのみを提供する。一方でstd::mdspanに指定するレイアウトポリシーの表現能力としては、タイル化レイアウト(tiled layout)や空間充填曲線(space-filling curve)といった複雑なメモリレイアウトも表現できる。

空間充填曲線の一種であるヒルベルト曲線(Hilbert curve)の実装例:*2

// C++2b(C++23)
#include <bit>
#include <mdspan>
#include <utility>

struct layout_hilbert {
  template <class Extents>
  class mapping {
  public:
    using extents_type = Extents;
    using index_type = typename extents_type::index_type;
    using rank_type = typename extents_type::rank_type;
    using layout_type = layout_hilbert;

    mapping(const extents_type& e)
      : extents_{e}
    {
      static_assert(extents_type::rank() == 2);
      assert(e.extent(0) == e.extent(1));
      assert(std::has_single_bit(e.extent(0)));
    }
    constexpr const extents_type& extents() const noexcept
      { return extents_; }
    template <class I0, class I1>
    constexpr index_type operator()(I0 i, I1 j) const noexcept
    {
      const index_type N = std::bit_ceil(extents_.extent(0));
      return xy2d(N, j, i);  // x,y=j,i
    }
    constexpr index_type required_span_size() const noexcept
    {
      const index_type N = std::bit_ceil(extents_.extent(0));
      return N * N;
    }
    static constexpr bool is_always_unique() noexcept { return true; }
    static constexpr bool is_always_exhaustive() noexcept { return true; }
    static constexpr bool is_always_strided() noexcept { return false; }
    static constexpr bool is_unique() noexcept { return true; }
    static constexpr bool is_exhaustive() noexcept { return true; }
    static constexpr bool is_strided() noexcept { return false; }
  private:
    // https://en.wikipedia.org/wiki/Hilbert_curve より
    static constexpr index_type xy2d(
      index_type n, index_type x, index_type y)
    {
      index_type rx, ry, s, d = 0;
      for (s = n / 2; s > 0; s /= 2) {
        rx = (x & s) > 0;
        ry = (y & s) > 0;
        d += s * s * ((3 * rx) ^ ry);
        rot(n, &x, &y, rx, ry);
      }
      return d;
    }
    static constexpr void rot(
      index_type n, index_type *x, index_type *y,
      index_type rx, index_type ry)
    {
      if (ry == 0) {
        if (rx == 1) {
          *x = n-1 - *x;
          *y = n-1 - *y;
        }
        std::swap(*x, *y);
      }
    }
  private:
    extents_type extents_{};
  };
};

int arr[16] = /*...*/;

// 4x4=16要素がヒルベルト曲線順に配置された2次元ビュー
using MatExts = std::dextents<size_t, 2>;
std::mdspan mat{arr, layout_hilbert::mapping{MatExts{4, 4}}};
// mat[i,j]⇒arr要素順
//   j | 0  1  2  3
// ----+------------
// i=0 | 1  2 15 16
//   1 | 4  3 14 13
//   2 | 5  8  9 12
//   3 | 6  7 10 11

layout_hilbert::mappingクラステンプレートによって、多次元インデクス(i,j)からメモリ領域へのオフセット位置への変換(operator())を定義している。変数mapはCTAD(→id:yohhoy:20230308)によりstd::mdspan<int, std::dextents<size_t, 2>, layout_hilbert>*3に推論される。

関連URL

*1:提案文書P0009R18 MDSPAN, §2.6 Why custom memory layouts? 参照。

*2:ここでは実現可能性を示すことが目的であり、簡単のため2次元ビュー(rank==2)かつ要素数(extent)が2の冪乗となる正方行列のみサポートする。原理的には非正方(non-square)行列や多次元ビュー(rank > 3)へ拡張可能。らしい。https://lutanho.net/pic2html/draw_sfc.html 参照。

*3:エイリアステンプレート std::dextents やデフォルトテンプレート引数を展開した厳密な型は std::mdspan<int, std::extents<size_t, std::dynamic_extent, std::dynamic_extent>, layout_hilbert, std::default_accessor<int>> となる。