オルトプラスエンジニアの日常をお伝えします!

量子コンピューティング++(1)

はじめに

こんにちは、最近はScalaにハマっているid:mitsutaka-takedaです。

MicrosoftやGoogleが続々と量子コンピュータのサービスやライブラリを公開しつつあります。数年後には「量子コンピュータ元年」が来るかもしれません。

C/C++でも量子コンピュータのシミュレータが多数実装されています。今回はgithubで公開されているQuantum++シミュレータで、量子コンピューティングの時代を少し覗いてみましょう。

Disclaimer

この記事は、量子コンピューティングを勉強し始めた素人が書いています。理解不足から間違えた記載が多々あると思いますので、参考資料を読んで下さい。

Qubit

現在のコンピュータの基本単位はbitで、量子コンピュータの基本単位はqubitです。

bitでは01かの2つの状態を取ります。一方、qubitも|0>|1>という2つの状態を取ります。 bitでは01か、どちらか2つの状態しか取れません。qubitは|0>|1>と、さらにその組み合わせの状態(superposition)を取ることができます。

ψ>=α0>+β1> |\\psi> = \\alpha |0> + \\beta |1>

α\\alphaβ\\betaは、ψ>|\\psi>を観測した時に0>|0>1>|1>になる確率を表現する係数です。確率を表現するため係数の絶対値の2乗の和が1となります(α2+β2=1|\\alpha|^2 + |\\beta|^2 = 1)。qubitは確率を含んだ状態であることと、bitのような特定の状態(0または1)にあるのを確定するにはqubitを観測する必要があることが、現在のコンピュータとの大きな違いです。

表と裏が出る確率が12\\frac{1}{2}づつの公平なコインをqubitで表現してみましょう。|0>を表、|1>を裏で表現すると、公平なコインは、α2=12|\\alpha|^2 = \\frac{1}{2}β2=12|\\beta|^2 = \\frac{1}{2}となるような係数α\\alphaβ\\betaで表現できます。

coin> =0>2+1>2 |coin> \\ = \\frac{|0>}{\\sqrt{2}} + \\frac{|1>}{\\sqrt{2}}

bitも複数つなげることができるようにqubitも複数つなげることができます。例えば、2 qubitは、0と1を2つ並べた|00>|01>|10>|11>の重ね合わせを表現することができます。確率的に4つの状態を取るものを表現できるため、例えば、出す手が3分の1づつのジャンケンは、|00>をグー、|01>をチョキ、|10>をパーとすると、以下のように表現できます。

rock paper cissors> =00>3+01>3+10>3+011> | rock\\ paper\\ cissors >\\ = \\frac{|00>}{\\sqrt{3}} + \\frac{|01>}{\\sqrt{3}} + \\frac{|10>}{\\sqrt{3}} + 0 \\cdot |11>

|11>に対応する手がないので係数は0になります。

ここで量子コンピュータをシミュレートするQuantum++を使用して、コインとジャンケンのqubitを表現するコードを書いてみます。

#include <iostream> #include "qpp.h" // Qubitsを表現するユーザ定義型リテラル。 template <char... Bits> qpp::ket operator "" _q(){ constexpr char bits[sizeof...(Bits) + 1] = {Bits..., '\0'}; qpp::ket q = qpp::ket::Zero(sizeof...(Bits)*2); q(std::stoi(bits, nullptr, 2)) = 1; return q; } int main() { // ( 表 ) + ( 裏 ) qpp::ket const coin = 0_q/std::sqrt(2) + 1_q/std::sqrt(2); std::cout << "= コイン =\n" << qpp::disp(coin) << '\n'; // ( グー ) + ( チョキ ) + ( パー ) qpp::ket const rock_paper_cissors = 00_q/std::sqrt(3) + 01_q/std::sqrt(3) + 10_q/std::sqrt(3); std::cout << "= ジャンケン =\n" << qpp::disp(rock_paper_cissors) << '\n'; }

このコードの実行結果は以下のようになります。実行結果の中で"#"以降は実行結果に追加したコメントです。標準出力にqubitを出力すると、各状態の係数が表示されます。120.707107\\frac{1}{\\sqrt {2}} \\approx 0.707107130.57735\\frac{1}{\\sqrt{3}} \\approx 0.57735です。

= コイン = 0.707107 # 表|0>の係数。 0.707107 # 裏|1>の係数。 = ジャンケン = 0.57735 # グー |00>の係数。 0.57735     # チョキ |01>の係数。 0.57735 # パー |10>の係数。 0 # 空き |11>の係数。

Qubitの観測

qubitは、複数の状態とその状態になりえる確率を掛けた重ね合わせの状態です。qubitを観測することで、qubitは特定のビットに収束します。

例えば、公平なコインの例では「表」|0>、「裏」|1>の出る割合が1/21/2でした。公平なコインを表現するqubitを観測すると1/21/2の確率で|0>、または、|1>に収束します。

coin>before=0>2+1>2 |coin>\_{before} = \\frac{|0>}{\\sqrt{2}} + \\frac{|1>}{\\sqrt{2}}
coin>measured={0>(1/2 probabilities)1>(1/2 probabilities) |coin>\_{measured} = \\begin{cases} |0> \\qquad (1/2\\space probabilities)\\\\ |1> \\qquad (1/2\\space probabilities) \\end{cases}

ジャンケンの場合は1/31/3づづの確率で「グー」、「チョキ」、「パー」を表現するビットに収束します。

rock paper cissors>before=00>3+01>3+10>3+011> |rock\\ paper\\ cissors>\_{before}\\enspace = \\frac{|00>}{\\sqrt{3}} + \\frac{|01>}{\\sqrt{3}} + \\frac{|10>}{\\sqrt{3}} + 0 \\cdot |11>
rock paper cissors>measured={00>(1/3 probabilities)01>(1/3 probabilities)10>(1/3 probabilities) |rock\\space paper\\space cissors>\_{measured}\\enspace = \\begin{cases} |00> \\qquad (1/3\\space probabilities)\\\\ |01> \\qquad (1/3\\space probabilities)\\\\ |10> \\qquad (1/3\\space probabilities) \\end{cases}

Quantum++で、コインを表現するqubitを観測してみます。

#include <iostream> #include "qpp.h" // Qubitsを表現するユーザ定義型リテラル。 template <char... Bits> qpp::ket operator "" _q(){ constexpr char bits[sizeof...(Bits) + 1] = {Bits..., '\0'}; qpp::ket q = qpp::ket::Zero(sizeof...(Bits)*2); q(std::stoi(bits, nullptr, 2)) = 1; return q; } int main() { // ( 表 ) + ( 裏 ) qpp::ket const coin = 0_q/std::sqrt(2) + 1_q/std::sqrt(2); // コインの表か裏か観測する。 // qpp::gt.Id2は、2-by-2の行列で、列ベクタは観測用のベクタ。1列目が|0>で|1>。 auto const& [result, ignore0, ignore1] = qpp::measure(coin, qpp::gt.Id2, {0}); std::cout << "観測結果 = " << (result == 0 ? "表" : "裏") << "\n"; // 1万回、観測して表と裏の回数を数える。 int heads_counter = 0, tails_counter = 0; for(int i = 0; i < 10000; ++i){ auto const& [result, ignore0, ignore1] = qpp::measure(coin, qpp::gt.Id2, {0}); (result == 0? heads_counter : tails_counter)++; } std::cout << "表の回数 = " << heads_counter << ", 裏の回数 = " << tails_counter << "\n"; }

以下が私の手元でのある実行の結果です。この結果は毎回実行するたびに異なりますが、表の回数、裏の回数はともに約5,000回になります。qubitは公平なコインを上手く表現できてるようです。

観測結果 = 裏 表の回数 = 5047, 裏の回数 = 4953

最後に

今回はQubitが確率的に表現されることと、観測することによって特定の状態に収束することをQuantam++で見てみました。次回は、qubitへの操作など見てみようと思います。

参考

ビルド・インストラクション

サンプル・コードはclang 5.0でビルドしています。qppをgithubからクローンして、Eigenをプロジェクト・ページからダウンロード。qppのディレクトリと同レベルにEigenのディレクトリを配置します。

以下のコマンドでqppがビルドできます。サンプルコードをビルドするには、qppのCMakeLists.txtを編集してサンプルコードをビルド対象に含めるか。

CC=clang CXX=clang++ cmake -DCMAKE_CXX_FLAGS="-std=c++17 -stdlib=libc++" -DWITH_OPENMP=OFF -DCMAKE_EXPORT_COMPILE_COMMANDS=ON ~/src/cpp/qpp/ && cmake --build ./

コピー無しでC IFを呼び出す

目標

こんにちは、最近はCppCon2017の動画を消化するために、睡眠不足気味なid:mitsutaka-takadaです。

CのIFでは互換性を守るためにOpaque Data Typeを使用していることがあります。

Oapque Data Typeを使用したIFでは、ライブラリとクライアントの間をやりとりする型は、サイズのみ クライアントに提供して実際の構造は隠しています。

今回はOpaque Data Typeを使用しているC IFと効率的にデータをやり取りする方法を考えます。

説明

以下のコードでは、OpaqueDataTypeは4バイトという情報のみをクライアントに提供しています。ライブラリの実装では、 それをintとして扱っています。

/** In header file **/
// ヘッダ・ファイルではサイズのみ。中身の構造は分からない。
struct OpaqueDataType{
  unsigned char data[4];
};

// 初期化関数でdataを初期化。
void initializeLibrary(OpaqueDataType* p);

// 使うときは、OpaqueDataTypeを引数に取る。
void add(OpaqueDataType* p, int x);
int getResult(OpaqueDataType* p);


/** In implementation file. **/
void initializeLibrary(OpaqueDataType* p){
  // dataをintとして扱う。
  int* sum = (int*)(p->data);
  *sum = 0;
}

void add(OpaqueDataType* p, int x){
  int* sum = (int*)(p->data);
  (*sum) += x;
}

int getResult(OpaqueDataType* p){
  return *((int*)(p->data));
}

/** Client **/

int main(){
  OpaqueDataType opaqueData;
  initializeLibrary(&opaqueData);
  add(&opaqueData, 1);
  add(&opaqueData, 2);

  assert(3 == getResult(&opaqueData));
}

C++からこのようなIFを扱うときは、上記のようにOpaqueDataTypeの変数を定義して、IFを呼び出すのが一般的かと思います。

ライブラリの使用は実装の詳細であるため、OpaqueDataTypeをクライアントのコードの色々なところで使用するのは好ましくありません。 std::vectorなどのbufferにOpaqueDataType::dataを保持してOpaqueDataTypeの型を消すことを考えます。

/** Client **/
#include <cassert>
#include <vector>

int main(){

  std::vector<unsigned char> buf(4);

  {// ブロック1
      OpaqueDataType opaqueData;
      initializeLibrary(&opaqueData);
      add(&opaqueData, 1);
      std::copy(std::begin(opaqueData.data), std::end(opaqueData.data), std::begin(buf));
  }

  {// ブロック2
    OpaqueDataType opaqueData;
    std::copy(std::begin(buf), std::end(buf), std::begin(opaqueData.data));
    add(&opaqueData, 2);
    // bufに必要なデータがコピーされているので、1つめのブロックで足された1も結果に反映されている。
    assert(3 == getResult(&opaqueData));
  }

}

意図通り、必要なデータがコピーされていますが、何度もバイト列のコピーが行なわれています。

このコピーを省いてstd::vectorを直接OpaqueDataTypeとして扱うことができるでしょうか。 OpaqueDataTypeも結局は4バイトのメモリ領域であるということ以外の情報は保持していません。

C++の標準ではStandardLayoutTypeというコンセプトがあります。 ある型TがStandardLayoutTypeであるとき、その第1メンバ変数へのポインタは安全にT*へのポインタとして扱うことができます。

今回のケースではOpaqueDataTypeがStandardLayoutTypeで、第1メンバ変数はdataになります。つまり、OpaqueDataTypeが、 StandardLayoutTypeであれば、unsigned char[4]と同等に扱えることになります。これをふまえて、std::vectorを直接 OpaqueDataTypeとして扱ってみます。

/** Client **/
#include <cassert>
#include <vector>
#include <type_traits>

// C++: コンパイル時に型情報を取得してassertする。
static_assert(std::is_standard_layout_v<OpaqueDataType>);

int main(){

  std::vector<unsigned char> buf(4);

  {// ブロック1
    // このreinterpret_castはOpaqueDataTypeがStandardLayoutTypeなら安全。
    auto opaqueDataPointer = reinterpret_cast<OpaqueDataType*>(buf.data());
    initializeLibrary(opaqueDataPointer);
    add(opaqueDataPointer, 1);
  }

  {// ブロック2
    auto opaqueDataPointer = reinterpret_cast<OpaqueDataType*>(buf.data());
    add(opaqueDataPointer, 2);
    assert(3 == getResult(opaqueDataPointer));
  }
}

無事コピーなしでstd::vectorOpaqueDataTypeとして扱うことができました。

C++ではtype_traitsというコンパイル時に型情報を取得するライブラリが用意されています。 static_assertOpaqueDataTypeがStandardLayoutTypeであることを確認しておきましょう。 ライブラリのアップデートでOpaqueDataTypeがStandardLayoutTypeで無くなってしまっても、 コンパイル・エラーで気付くことができます。

std::is_standard_layoutリファレンス@cpprefjp

最後に

今回はOpaque Data Typeを使用しているC IFと効率的にデータを交換する方法を見てきました。

StandardLayoutTypeであれば、reinterpret_castで安全にキャストができます。reinterpret_castは、適切に使えないと 危険ですが強力なツールです。type_traitsstatic_castを利用して安全性を高めましょう。

エラー・コードと型消去(Type Erasure)

こんにちは、id:mitsutaka-takadaです。

C++でエラー通知というとエラー・コードや例外が通常の手段かと思います。エラー・コードは戻り値でenumを返すことでエラーを通知します。 例外と比較してenumオブジェクトを返すのみで、とても軽量な通知手段です。

今回の記事では複数のエラー・コードを型消去によってまとめて扱う方法を書きたいと思います。

ゴール

例えば、ネットワーク・リクエストとデータベース・アクセスをしている関数があるとします。それぞれ自身のドメインに関するエラーをエラー・コード(NetworkError&DatabaseError)で通知してくるとき、この関数はエラー・コード(SomeError)として何を返せばよいでしょうか?

#include <string>
#include <experimental/string_view>

// ネットワーク関連のエラー・コード。
enum class NetworkError{
    NoError,
    SomeNetworkError
};

// データベース関連のエラー・コード。
enum class DatabaseError{
    NoError,
    SomeDatabaseError
};

std::pair<std::string, DatabaseError>
getAccountIdFromDatabase(std::string_view userId){
    return {"", DatabaseError::SomeDatabaseError};
}

std::pair<int, NetworkError>
getAccountBalanceFromNetwork(std::string_view accountId){
    return {-1, NetworkError::SomeNetworkError};
}

// エラー情報。ネットワーク&データベースの両方のエラー情報を返したい。
struct SomeError{};

std::pair<int, SomeError>
getUserBalance(std::string_view userId){

    if(auto [accountId, databaseError] = getAccountIdFromDatabase(userId);
       databaseError == DatabaseError::SomeDatabaseError){
        // データベース関連のエラー情報を返したい
        return {};
    }
    else if(auto [balance, networkError] = getAccountBalanceFromNetwork(accountId);               networkError == NetworkError::SomeNetworkError){
        // ネットワーク関連のエラー情報を返したい
        return {};
    }
    else {
        return {balance, {}};
    }
}

<system_error>

エラーコードの和集合、Variantを利用する(下記、おまけ参照)など、いくつか考えられると思いますが、 今回はC++11で導入された<system_error>を利用した方式を紹介したいと思います。

SomeErrorNetworkErrorDatabaseErrorが代入でき(型消去)、SomeErrorからその情報を取得するのが目標です。

<system_error>では、複数ドメインのエラーコードを1つのenumとして統一的に扱うためstd::error_codeを用意しています。std::error_codeはエラーコード(整数値)とカテゴリ(ドメインを表すstd::error_categoryオブジェクト)の対です。エラーコードは整数値で複数ドメインをまたいで一意性が保証されていません。例えば、NetworkError::SomeErrorDatabaseError::SomeErrorに同じ1という値が割り当てられているかもしれません。そのため単純に整数値を比較するだけではNetworkError::SomeErrorDatabaseError::SomeErrorを識別できなくなります。これを防ぐためにカテゴリを使用します。

std::error_codeを使用するには、以下の3ステップが必要です。

  1. エラーコードをstd::error_codeで使用できるように登録する。(std::is_error_code_enumの特殊化)
  2. ドメイン用のカテゴリを定義する。(std::error_categoryの派生クラスの定義)
  3. エラーコードとカテゴリの紐づけを行う。(make_error_codeオーバーロードの定義)

実際にstd::error_codeを使用したコードを見てみましょう。

#include <iostream>
#include <string>
#include <experimental/string_view>
#include <system_error>

enum class NetworkError{
    NoError,
    SomeNetworkError
};

enum class DatabaseError{
    NoError,
    SomeDatabaseError
};

// 1. エラーコードをstd::error_codeで利用できるように登録。
namespace std{
    template<>
    struct std::is_error_code_enum<NetworkError> : std::true_type{};

    template<>
    struct std::is_error_code_enum<DatabaseError> : std::true_type{};
}

// 2. ドメイン用カテゴリの定義。
struct NetworkErrorCategory : std::error_category{
    const char* name() const noexcept override{
        return "NetworkErrorCategory";
    }

    std::string message(int ev) const override{
        switch(static_cast<NetworkError>(ev)){
            case NetworkError::SomeNetworkError:
                return "some network error occured";
            case NetworkError::NoError:
                return "no error";
        }
    }
};

struct DatabaseErrorCategory : std::error_category{
    const char* name() const noexcept override{
        return "DatabaseErrorCategory";
    }

    std::string message(int ev) const override{
        switch(static_cast<NetworkError>(ev)){
            case NetworkError::SomeNetworkError:
                return "some database error occured";
            case NetworkError::NoError:
                return "no error";
        }
    }
};

// カテゴリオブジェクトの比較にアドレス比較を用いるため、
// カテゴリオブジェクトはシングルトンでなければいけません!
NetworkErrorCategory const networkErrorCategoryInstance;
DatabaseErrorCategory const databaseErrorCategoryInstance;

// 3. エラーコードとカテゴリの紐づけ。
inline
std::error_code make_error_code(NetworkError ne){
    return {static_cast<std::underlying_type_t<NetworkError>>(ne), networkErrorCategoryInstance};
}

inline
std::error_code make_error_code(DatabaseError de){
    return {static_cast<std::underlying_type_t<DatabaseError>>(de), databaseErrorCategoryInstance};
}

std::pair<std::string, DatabaseError>
getAccountIdFromDatabase(std::string_view userId){
    return {"", DatabaseError::SomeDatabaseError};
}

std::pair<int, NetworkError>
getAccountBalanceFromNetwork(std::string_view accountId){
    return {-1, NetworkError::SomeNetworkError};
}

std::pair<int, std::error_code>
getUserBalance(std::string_view userId){
    if(auto [accountId, databaseError] = getAccountIdFromDatabase(userId);
        databaseError == DatabaseError::SomeDatabaseError){
        return {-1, databaseError};
    }
    else if(auto [balance, networkError] = getAccountBalanceFromNetwork(accountId);
            networkError == NetworkError::SomeNetworkError) {
        return {-1, networkError};
    }
    else {
        // Successを表現するにはデフォルト・コンストラクタを使用する。
        return {balance, std::error_code{}};
    }
}

int main(){

    if(auto const [balance, error] = getUserBalance("mitsutaka-takeda");
       !error // エラーがあるときは、std::error_codeオブジェクトがtrueになる。
       ){
        // 成功!
        std::cout << "my balance is " << balance << std::endl;
    }
    else{
        // 失敗!
        if(error == NetworkError::SomeNetworkError){
           // handle network error!
        }
        else if(error == DatabaseError::SomeDatabaseError){
           // handle database error!
        }
    }
}

まず、getUserBalanceで複数ドメインのエラーをstd::error_codeとして統一できていることに注目してください。各ドメインのエラーコードNetworkErrorDatabaseErrorからstd::error_codeへの変換は暗黙的に行われます。

またmain関数のエラーハンドリングで、std::error_codeからエラー情報を取得する際、各ドメインのエラーコード(NetworkError::SomeNetworkErrorDatabaseError::SomeDatabaseError)と直接比較しています。

std::error_code自体はポリモーフィズムも利用せず整数値とオブジェクトへの参照の対で軽量な構造体であり、既存のエラーコードに非侵入的に使用できるため色々な場面で活躍できます。またC++の標準ライブラリでも使用されており統一されたエラーハンドリングを行うための基礎になります。

他にも複数のエラーコードをグルーピングするstd::error_conditionなど応用もあるので興味がある方は参考のリンクを見てください。

参考

おまけ

エラーコードの和集合

SomeErrorNetworkErrorDatabaseErrorの値の和として定義することで、2つのエラー情報を持つエラーコードを返すことができます。SomeErrorNetworkErrorDatabaseErrorのコードに対応するコードをすべて追加して、NetworkError/DatabaseErrorからSomeErrorへの変換処理fromNetworkError/fromDatabaseErrorを書きます。

std::error_codeと比較すると、各ドメインのエラーコードへの修正がSomeErrorなど他の箇所にも影響を与えます。

#include <string>
#include <experimental/string_view>

enum class NetworkError{
    NoError,
    SomeNetworkError
};

enum class DatabaseError{
    NoError,
    SomeDatabaseError
};

// ネットワークとデータベースのエラーコードの和集合。
enum class SomeError{
    NoError,
    SomeNetworkError,
    SomeDatabaseError
};

SomeError
fromNetworkError(NetworkError networkError){
    // NetworkErrorからSomeErrorへの変換。
    return networkError == NetworkError::NoError ? SomeError::NoError : SomeError::SomeNetworkError;
}

SomeError
fromDatabaseError(DatabaseError databaseError){
    // DatabaseErrorからSomeErrorへの変換。
    return databaseError == DatabaseError::NoError ? SomeError::NoError : SomeError::SomeDatabaseError;
}

std::pair<std::string, DatabaseError>
getAccountIdFromDatabase(std::string_view userId){
    return {"", DatabaseError::SomeDatabaseError};
}

std::pair<int, NetworkError>
getAccountBalanceFromNetwork(std::string_view accountId){
    return {-1, NetworkError::SomeNetworkError};
}

std::pair<int, SomeError>
getUserBalance(std::string_view userId){
    if(auto [accountId, databaseError] = getAccountIdFromDatabase(userId);
       databaseError == DatabaseError::SomeDatabaseError){
        // DatabaseErrorをSomeErrorに変換して返す。
        return {-1, fromDatabaseError(databaseError)};
    }
    else if(auto [balance, networkError] = getAccountBalanceFromNetwork(accountId);
            networkError == NetworkError::SomeNetworkError) {
        // NetworkErrorをSomeErrorに変換して返す。
        return {-1, fromNetworkError(networkError)};
    }
    else {
        return {balance, SomeError::NoError};
    }
}

C++17 std::variant

SomeErrorNetworkError/DatabaseErrorのC++17で導入されるvariantとして定義する方法です。和集合と比べて、対応する値の定義や変換処理は不要になります。

std::error_codeを使用した方法と比較すると、SomeErrorの型を事前に決定しておかなければいけません。 例えば、getUserBalanceが他ドメイン(ファイルシステム)のエラーを追加で扱わなければいけないとき、 SomeErrorの定義をstd::variant<NoError, NetworkError, DatabaseError, FileSystemError>に変更しなければいけません。std::error_codeでは、型情報は消去されているので、そのような修正入りません。

#include <string>
#include <experimental/string_view>
#include <variant>

enum class NetworkError{
    NoError,
    SomeNetworkError
};

enum class DatabaseError{
    NoError,
    SomeDatabaseError
};

// ネットワークとデータベースのエラーコードのVariant。
struct NoError{};
using SomeError = std::variant<NoError, NetworkError, DatabaseError>;

std::pair<std::string, DatabaseError>
getAccountIdFromDatabase(std::string_view userId){
    return {"", DatabaseError::SomeDatabaseError};
}

std::pair<int, NetworkError>
getAccountBalanceFromNetwork(std::string_view accountId){
    return {-1, NetworkError::SomeNetworkError};
}

std::pair<int, SomeError>
getUserBalance(std::string_view userId){
    if(auto [accountId, databaseError] = getAccountIdFromDatabase(userId);
        databaseError == DatabaseError::SomeDatabaseError){
        // DatabaseErrorをSomeErrorに変換して返す。
        return {-1, databaseError};
    }
    else if(auto [balance, networkError] = getAccountBalanceFromNetwork(accountId);
            networkError == NetworkError::SomeNetworkError) {
        // NetworkErrorをSomeErrorに変換して返す。
        return {-1, networkError};
    }
    else {
        return {balance, NoError{}};
    }
}