Правильное распространение переменной 'decltype (auto)' из функции

(Это продолжение "Есть ли реальные варианты использования переменных 'decltype (auto)'?")

Рассмотрим следующий сценарий - я хочу передать функцию f другой функции invoke_log_return, которая будет:

  1. Вызвать f;

  2. Напечатайте что-нибудь на стандартный вывод;

  3. Вернуть результат f, избегая ненужных копий/перемещений и допуская копирование.

Обратите внимание, что если выбрасывает f, ничего не должно быть напечатано на стандартный вывод. Это то, что я до сих пор:

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;
    }
}

Давайте рассмотрим различные возможности:

  • Когда f возвращает значение:

    • result будет объектом;

    • invoke_log_return(f) будет prvalue (право на получение копии).

  • Когда f возвращает lvalue или xvalue:

    • result будет ссылкой;

    • invoke_log_return(f) будет lvalue или xvalue.

Вы можете увидеть тестовое приложение здесь, на godbolt.org. Как видите, g++ выполняет NRVO для случая prvalue, а clang++ - нет.

Вопросы:

  • Является ли это кратчайшим способом "идеального" возврата переменной decltype(auto) из функции? Есть ли более простой способ добиться того, чего я хочу?

  • Можно ли извлечь шаблон if constexpr { ... } else { ... } в отдельную функцию? Единственный способ извлечь его - это макрос.

  • Есть ли веская причина, по которой clang++ не выполняет NRVO для описанного выше случая предварительной оценки? Следует ли сообщать о нем как о потенциальном улучшении, или оптимизация g++ NRVO здесь недопустима?


Вот альтернатива с использованием помощника on_scope_success (как предложил Барри Ревзин):

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/copy-elision с этим решением.

живой пример на godbolt.org

Как отметил Бен Фойгт, одним из основных недостатков этого подхода является то, что возвращаемое значение f не может быть частью сообщения журнала.

Ответ 1

Мы можем использовать модифицированную версию 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 возвращает тип без ссылки;

  • lvalue, тогда result является ссылкой на lvalue, а invoke_log_return возвращает ссылочный тип lvalue;

  • xvalue, тогда result является ссылкой на rvalue, а invoke_log_return возвращает тип ссылки на rvalue.

(По сути скопировано с моего fooobar.com/questions/18326191/...)

Ответ 2

Это самый простой и понятный способ написать это:

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!

Таким образом, если код понятен, он имеет те же функциональные возможности, но не оптимизирован для работы столько, сколько конкуренты делают это при сбое оптимизации компилятора, и clang должен решить эту проблему. Такая проблема, которая имеет больше смысла, решается в инструменте, а не на уровне приложения.

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