web-dev-qa-db-ja.com

関数から `decltype(auto)`変数を正しく伝播する

(これはからのフォローアップです) " ` decltype(auto) `変数の現実的な使用例はありますか? "

次のシナリオを考えてみましょう-関数fを別の関数_invoke_log_return_に渡して、次のようにします。

  1. f;を呼び出す

  2. stdoutに出力します

  3. fの結果を返し、不要なコピー/移動を回避し、コピーの省略を許可します。

fがスローした場合、stdoutには何も出力されないことに注意してください。これは私がこれまでに持っているものです:

_template <typename F>
decltype(auto) invoke_log_return(F&& f)
{
    decltype(auto) result{std::forward<F>(f)()};
    std::printf("    ...logging here...\n");

    if constexpr(std::is_reference_v<decltype(result)>)
    {
        return decltype(result)(result);
    }
    else
    {
        return result;
    }
}
_

さまざまな可能性を考えてみましょう:

  • fprvalueを返す場合:

    • resultはオブジェクトになります。

    • invoke_log_return(f)prvalueになります(コピー省略の対象)。

  • flvalueまたはxvalueを返す場合:

    • resultは参照になります。

    • invoke_log_return(f)lvalueまたはxvalueになります。

テストアプリケーションは こちらのgodbolt.org で確認できます。ご覧のとおり、_g++_はprvalueの場合にNRVOを実行しますが、_clang++_は実行しません。

質問:

  • これは、関数からdecltype(auto)変数を「完全に」返す最も短い方法ですか?達成する簡単な方法はありますか?私が欲しいものは?

  • _if constexpr { ... } else { ... }_パターンを別の関数に抽出できますか?それを抽出する唯一の方法はマクロのようです。

  • 上記のprvalueケースで_clang++_がNRVOを実行しない理由はありますか?潜在的な機能強化として報告する必要がありますか、それとも_g++_のNRVO最適化はここでは無効ですか?


_on_scope_success_ヘルパーを使用する別の方法を次に示します(Barry Revzinが提案)。

_template <typename F>
struct on_scope_success : F
{
    int _uncaught{std::uncaught_exceptions()};

    on_scope_success(F&& f) : F{std::forward<F>(f)} { }

    ~on_scope_success()
    {
        if(_uncaught == std::uncaught_exceptions()) {
            (*this)();
        }
    }
};

template <typename F>
decltype(auto) invoke_log_return_scope(F&& f)
{
    on_scope_success _{[]{ std::printf("    ...logging here...\n"); }};
    return std::forward<F>(f)();
}
_

_invoke_log_return_scope_ははるかに短いですが、これには関数の動作の異なるメンタルモデルと新しい抽象化の実装が必要です。驚くべきことに、_g++_と_clang++_の両方が、このソリューションでRVO /コピー削除を実行します。

godbolt.orgのライブ例

Ben Voigt で言及されているように、このアプローチの主な欠点の1つは、fの戻り値をログメッセージの一部にすることができないことです。

24
Vittorio Romeo

_std::forward_の修正バージョンを使用できます(ADLの問題を防ぐために、名前の転送は避けられます)。

_template <typename T>
T my_forward(std::remove_reference_t<T>& arg)
{
    return std::forward<T>(arg);
}
_

この関数テンプレートは、decltype(auto)変数を転送するために使用されます。次のように使用できます。

_template <typename F>
decltype(auto) invoke_log_return(F&& f)
{
    decltype(auto) result{std::forward<F>(f)()};
    std::printf("    ...logging here...\n");
    return my_forward<decltype(result)>(result);
}
_

このように、std::forward<F>(f)()が返す場合

  • prvalueの場合、resultは非参照であり、_invoke_log_return_は非参照型を返します。

  • 左辺値の場合、resultは左辺値参照であり、_invoke_log_return_は左辺値参照型を返します。

  • xvalueの場合、resultは右辺値参照であり、_invoke_log_return_は右辺値参照型を返します。

(基本的には https://stackoverflow.com/a/57440814 からコピーされます)

1
L. F.

これが最も簡単で明確な方法です。

template <typename F>
auto invoke_log_return(F&& f)
{ 
    auto result = f();
    std::printf("    ...logging here... %s\n", result.foo());    
    return result;
}

GCCは正しい(不必要なコピーや移動はありません)期待される結果を取得します。

    s()

in main

prvalue
    s()
    ...logging here... Foo!

lvalue
    s(const s&)
    ...logging here... Foo!

xvalue
    s(s&&)
    ...logging here... Foo!

したがって、コードが明確な場合は、同じ機能を使用しますが、競合他社が実行するのと同じくらいに実行するように最適化されていません。これは、アプリケーション層の実装ではなく、ツールで解決する方が理にかなった種類の問題です。

https://gcc.godbolt.org/z/50u-hT