yohhoyの日記

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

{void,value,both}-compatibleラムダ式

プログラミング言語Javaにおけるラムダ式は、その本体部に応じてvoid-compatible/value-compatible/その両方に区分される。

void-compatibleラムダ式Runnableなどの戻り値を持たない(void)関数型インタフェース(functional interface)へ、value-compatibleラムダ式IntSupplierなどの戻り値をもつ関数型インタフェースへと代入できる。

import java.util.function.IntSupplier;

// void-compatible
Runnable    r1 = () -> { };  // OK
IntSupplier s1 = () -> { };  // NG

// value-compatible
Runnable    r2 = () -> { return 42; };  // NG
IntSupplier s2 = () -> { return 42; };  // OK

// void-compatible かつ value-compatible
Runnable    r3 = () -> { throw new NullPointerException(); };  // OK
IntSupplier s3 = () -> { throw new NullPointerException(); };  // OK
final boolean TRUE = true;
Runnable    r4 = () -> { while (TRUE); };  // OK
IntSupplier s4 = () -> { while (TRUE); };  // OK

Java Language Specification, Java SE 8 Editionより一部引用。

The rules in this section define two technical terms:

  • whether a statement is reachable
  • whether a statement can complete normally

(snip)

The rules are as follows:

  • (snip)
  • A while statement can complete normally iff at least one of the following is true:
    • The while statement is reachable and the condition expression is not a constant expression (§15.28) with value true.
    • There is a reachable break statement that exits the while statement.
  • (snip)
  • A break, continue, return, or throw statement cannot complete normally.
  • (snip)
Chapter 14. Blocks and Statements, 14.21. Unreachable Statements

A block lambda body is void-compatible if every return statement in the block has the form return;.

A block lambda body is value-compatible if it cannot complete normally (§14.21) and every return statement in the block has the form return Expression;.

(snip)

Note that the void/value-compatible definition is not a strictly structural property: "can complete normally" depends on the values of constant expressions, and these may include names that reference constant variables.

Chapter 15. Expressions, 15.27.2. Lambda Body

関連URL

returns_twice属性

GCCとClangの独自拡張 returns_twice 属性についてメモ。

対象関数から「2回以上制御が戻ってくる可能性」*1コンパイラに伝える属性。コンパイラによる一部の最適化処理を抑止する。

The returns_twice attribute tells the compiler that a function may return more than one time. The compiler ensures that all registers are dead before calling such a function and emits a warning about the variables that may be clobbered after the second return from the function. Examples of such functions are setjmp and vfork. The longjmp-like counterpart of such function, if any, might need to be marked with the noreturn attribute.

https://gcc.gnu.org/onlinedocs/gcc/Common-Function-Attributes.html

returns_twice
This attribute indicates that this function can return twice. The C setjmp is an example of such a function. The compiler disables some optimizations (like tail calls) in the caller of these functions.

https://llvm.org/docs/LangRef.html

関連URL

*1:関数から制御が戻ってこないことを表明するnoreturn属性と対をなす。

-fimplicit-constexprオプション

gcc(g++) 12.1にて、inline関数に対して暗黙に constexpr 指定を行うコンパイルオプション -fimplicit-constexpr が導入された。同オプションはC++14/17/20言語仕様に対する独自拡張として機能する。

With each successive C++ standard the restrictions on the use of the constexpr keyword for functions get weaker and weaker; it recently occurred to me that it is heading toward the same fate as the C register keyword, which was once useful for optimization but became obsolete. Similarly, it seems to me that we should be able to just treat inlines as constexpr functions and not make people add the extra keyword everywhere.

https://gcc.gnu.org/git/?p=gcc.git;a=commitdiff;h=87c2080b

関連URL

連番配列を生成

JavaScript(ECMAScript)で連番配列を生成するコード片。いわゆる range 関数に相当。

// NG: [undefined, undefined, undefined, undefined, undefined]
// { length: 5 } のみ設定されたArrayオブジェクトが生成される
// 各要素は未設定(undefinedとも異なる状態)となっている
Array(5).map((val, idx) => idx);
[,,,,,].map((val, idx) => idx);

// OK: [0, 1, 2, 3, 4]
Array(5).fill(undefined).map((val, idx) => idx);
Array(5).fill(0).map((val, idx) => idx);
Array.from(Array(5), (val, idx) => idx);
Array.from({length: 5}, (val, idx) => idx);
[...Array(5)].map((val, idx) => idx);
[...Array(5).keys()];

関連URL

std::expected<T, E>

次期C++2b(C++23)標準ライブラリに追加されるstd::expected<T, E>クラステンプレートについてメモ。

  • 従来C++例外機構(throw文+try/catch構文)に代わり、関数の演算結果(T型)またはエラー情報(E型)を “値” として返す(return)ための部品。
  • C++17 std::optional<T>型の無効値(std::nullopt)表現に代わり、具体的なエラー情報(E型)を保持可能に拡張された型。*1
  • 新しい標準ヘッダ<expected>
    • クラステンプレートexpected<T, E>、補助型unexpected<E>*2、タグunexpect、例外型bad_expected_access<E>
  • expected<T, E> == 結果Tとエラーunexpected<E>の直和型*3
    • constexpr文脈でも利用可能
    • expected<void, E>特殊化 == エラーのみを保持する
    • std::optional同様に参照型は未サポート
  • expected<T, E>型への代入:
    • 結果/Tへ変換可能な型:r = t;またはr = {std::in_place, t};*4
    • エラー/Eへ変換可能な型:r = std::unexpected(e);またはr = {std::unexpect, e};
  • 保持値アクセス:
    • 結果/エラー保持判定:has_value, explicit operator bool*5
    • 結果(T型):value, value_or, operator->, operator*
    • エラー(E型):error
    • 結果未保持でのvalueメンバ関数呼び出しはstd::bad_expected_access<E>{error()}例外送出
    • 結果未保持での間接参照(->, *)アクセスは未定義動作(undefined behavior)*6
    • エラー未保持でのerrorメンバ関数呼び出しは未定義動作
  • expectedモナディック(monadic)操作はC++2bには間に合わず*7C++2c(C++26)向け提案文書 P2505R1 にて継続検討中。
    • C++2b標準ライブラリoptionalモナディック操作(and_then, or_elseなど)は P0789R8 が採択済み。

Boost.Outcomeライブラリとの比較:

関連URL

*1:std::optional<T> ≒ std::expected<T, std::nullopt_t>

*2:C++20現在、識別子 unexpected はZombie nameとして予約されおり、expected<T, E> 導入に伴ってC++2bで蘇生(?)された初のケース。C++的ゾンビのお名前 参照。

*3:無効値 nullopt でデフォルト構築される std::optional<T> とは異なり、エラー情報を明示的に保持する std::expected<T, E> ではデフォルト構築後は結果値 T{} を保持している。

*4:https://cpprefjp.github.io/reference/utility/in_place_t.html

*5:if文の条件式などに expected<T, E> 型の値を記述することで、結果を保持しているか否かを判定可能(→id:yohhoy:20121110

*6:提案文書P0323R12 §3.14: "Using the indirection operator for an object that does not contain a value is undefined behavior. This behavior offers maximum runtime performance."

*7:https://github.com/cplusplus/papers/issues/1161#issuecomment-1027125553

コンセプト制約式の構成:包摂関係 vs. コンパイル時間

C++20コンセプトでは制約式(constraint-expression)を&&(conjunction)/||(disjunction)で組み合わせることで複雑な制約式を表現できる。一方で制約式が多数の原始制約(atomic constraint)から構成されるケースでは、包摂関係(subsumption relation)判定のための正規化プロセスが複雑になりコンパイル時間に悪影響を及ぼす。

例えばコンセプトX, Y, Zからなるコンセプトの定義方法として、下記2種類が考えられる。コンセプトC1は3個の原始制約から、一見すると記述が冗長なコンセプトC2は2個の原始制約から構成されるため、包摂関係が重要でなければ後者C2の方がC++コンパイラへの負荷が小さい。

template <typename T> concept X = /*(原始制約#1)*/;
template <typename T> concept Y = /*(原始制約#2)*/;
template <typename T> concept Z = /*(原始制約#3)*/;

template <typename T>
concept C1 = X<T> && (Y<T> || Z<T>);
//           ^^^^     ^^^^    ^^^^
//            #1       #2      #3

template <typename T>
concept C2 = X<T> && requires { requires (Y<T> || Z<T>); };
//           ^^^^    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
//            #1     原始制約#4

コンセプトC1C2単体利用であれば両者の意味論に差異はない。C1は制約X && Y, X && Z, Y || Zとの間に包摂関係が成り立つが*1C2ではいずれの包摂関係も成り立たない。*2

// X,Y,Zコンセプトを満たすU型を仮定
template <typename T> requires C1<T>
int f(T) { return 1; }
template <typename T> requires X<T> && Y<T>
int f(T) { return 2; }
assert( f(U{}) == 2 );

template <typename T> requires C2<T>
int g(T) { return 1; }
template <typename T> requires X<T> && Y<T>
int g(T) { return 2; }
// g(U{}) はオーバーロード解決が曖昧

実際にC++2b(C++23)向け提案文書(PDF)P2404R1では、コンパイル時間短縮を目的としてコンセプト定義の変更を行っている。

3.1.1 Avoiding disjunctions
Disjunctions in concepts may slow down compilation times due to added costs in the conversion to disjunctive normal form, so it is desirable to avoid them. As such, this new disjunction should be made into an atomic constraint to avoid this issue. Lost functionality is not a concern in this case, because users are not expected to find it useful to have subsumption between equality_comparable_with<T, U> and convertible_to<const T&, const common_reference_t<const T&, const U&>> or the other similar cases.
Avoiding the disjunctions in this case is easily resolved by forcing the disjunction to be an atomic constraint via a requires expression and nested requirements:

requires {
  requires convertible_to<const T&, const C&> || convertible_to<T&&, const C&>;
}

In this particular case, we have two disjunctions, one for T and one for U. Having a conjunction between atomic constraints is not useful, so the proposed wording will merge them into a single atomic constraint.

提案文書P2304R0およびP2404R1より、common-comparison-supertype-with説明用コンセプト定義を改変引用(メタ関数への引数部は略記)。

// P2304R0
template<class T, class U>
concept common-comparison-supertype-with =
  same_as</*...*/> &&
  (convertible_to</*...*/> || convertible_to</*...*/>) &&
  (convertible_to</*...*/> || convertible_to</*...*/>);

// P2404R1
template<class T, class U>
concept common-comparison-supertype-with =
  same_as</*...*/> &&
  requires {
    requires convertible_to</*...*/> || convertible_to</*...*/>;
    requires convertible_to</*...*/> || convertible_to</*...*/>;
  };

前者のコンセプト定義では10個の原始制約から構成されるのに対し、後者では3個の原始制約(&&に続くrequires式全体で1つの原始制約)から構成される。*3

関連URL

*1:https://wandbox.org/permlink/6l0UgFuTWECJZ9tM

*2:コンセプト C1 と コンセプト X、コンセプト C2 と コンセプト X の間にはそれぞれ包摂関係が成り立つ。https://wandbox.org/permlink/wpodIgRHXuNiVZI6

*3:C++20標準ライブラリ提供の std::same_as コンセプトは2個の原始制約(→id:yohhoy:20190925)、std::convertible_to コンセプトは2個の原始制約から構成される。

*4:2022年2月現在、C++ Core Guidelines ルールT.31はタイトルのみで説明文は未記述。

std::views::commonレンジアダプタの制約

C++20 RangeからC++17互換イテレータペアへの変換にはstd::views::commonレンジアダプタ(range adaptor)を利用する。ただしstd::ranges::basic_istream_viewなどムーブのみ/コピー不可なイテレータからなる一部Rangeは変換できない。*1

#include <sstream>
#include <ranges>
#include <vector>

auto ints = std::istringstream{"0 1 2 3 4"};
auto cv = std::ranges::istream_view<int>(ints)
        | std::views::filter([](int n){ return n % 2 == 0; })
        | std::views::common;  // NG: ill-formed
std::vector<int> vec(cv.begin(), cv.end());

これはstd::ranges::common_view<V>クラステンプレート制約として、対象Viewのイテレータ型(iterator_t<V>)がstd::copyableコンセプトを満たすことを要求するため。C++17互換イテレータではコピー可能・ムーブ可能を要求するが、C++20 Rangeを構成するイテレータはコピー不可・ムーブ可能であればよい。*2

前掲コードのようにRange/Viewからコンテナへ変換したい場合、次期C++2b(C++23)標準ライブラリではstd::views::toレンジアダプタ(→id:yohhoy:20210902)が利用可能となる。

// C++2b(C++23)
#include <sstream>
#include <ranges>
#include <vector>

auto ints = std::istringstream{"0 1 2 3 4"};
auto vec = std::ranges::istream_view<int>(ints)
         | std::views::filter([](int n){ return n % 2 == 0; })
         | std::views::to<std::vector>();
// vec == std::vector<int>{0, 2, 4}

C++20 24.6.4.3, 24.7.13.1, 24.7.13.2より一部引用。

namespace std::ranges {
  template<movable Val, class CharT, class Traits>
    requires default_initializable<Val> &&
             stream-extractable<Val, CharT, Traits>
  class basic_istream_view<Val, CharT, Traits>::iterator {
  public:
    // (snip)
    iterator(const iterator&) = delete;
    iterator(iterator&&) = default;
    iterator& operator=(const iterator&) = delete;
    iterator& operator=(iterator&&) = default;
    // (snip)
  };
}

1 common_view takes a view which has different types for its iterator and sentinel and turns it into a view of the same elements with an iterator and sentinel of the same type.
2 [Note: common_view is useful for calling legacy algorithms that expect a range's iterator and sentinel types to be the same. --end note]
3 The name views::common denotes a range adaptor object (24.7.1). (snip)

namespace std::ranges {
  template<view V>
    requires (!common_range<V> && copyable<iterator_t<V>>)
  class common_view : public view_interface<common_view<V>> {
    // (snip)
  };
}

関連URL

*1:std::ranges::istream_view<Val> は std::ranges::basic_istream_view<Val, char> のエイリアステンプレート。(PDF)P2432R1によりC++20 DRとして遡及適用。

*2:現行gcc/libstdc++は難解なエラーメッセージを生成する https://wandbox.org/permlink/qu5Hl1Lbj6fdhQot