Я реализовал функцию на C++, а именно вызываемый объект класса, предназначенный для возврата std::tuple
, содержащего контейнер объектов и результат. После некоторых корректировок (см. ниже) у меня есть определение функции-члена:
template <typename UserGroups>
auto operator()(const UserGroups& userGroups) const noexcept
{
using result_t = std::tuple<std::unordered_set<QnUuid>, Result>;
State state{.inheritingGroups = {m_rootId}};
for (const auto& group : userGroups)
{
if (Result res = dfs(state, group); !res)
return result_t({}, std::move(res));
}
return result_t(std::move(state.inheritingGroups), {});
}
... где State
:
struct State
{
std::unordered_set<QnUuid> inheritingGroups{};
std::unordered_set<QnUuid> visited{};
std::unordered_set<QnUuid> visiting{};
};
Clang-Tidy предупреждает меня по обоим утверждениям return
:
Clang-Tidy: передача результата std::move() в качестве аргумента константной ссылки; никакого движения на самом деле не произойдет
Чтобы решить эту проблему, я явно передал Result()
во втором операторе return
:
return result_t(std::move(state.inheritingGroups), Result());
Я подозреваю, что это может быть связано с выбором нешаблонного конструктора std::tuple
, когда {}
используется в качестве одного из аргументов. В идеале я стремлюсь к упрощенному синтаксису возврата, например:
// auto operator()(...) const -> std::tuple<std::unordered_set<QnUuid>, Result>
// If error occurs
if (Result res = dfs(state, group); !res)
return {{}, std::move(res)}; // std::move is required since NRVO will not work
// On success
return {std::move(state.inheritingGroups), {}};
Однако инициализация кортежа с помощью {...}
приводит к проблемам компиляции из-за неоднозначных вызовов конструктора.
Пытаясь решить эту проблему, я ввел псевдоним typedef, но это привело к более подробной и, на мой взгляд, менее чистой версии:
using groups = std::unordered_set<QnUuid>;
using result_t = std::tuple<groups, Result>;
// If error occurs
if (Result res = dfs(state, group); !res)
return result_t(groups(), std::move(res));
// On success
return result_t(std::move(state.inheritingGroups), Result());
Я считаю введение псевдонима groups
неоптимальным решением, поскольку оно служит только для обхода предупреждения Clang-Tidy и вносит ненужную сложность. Псевдоним должен быть объявлен за пределами непосредственной области видимости (State
используется двумя функциями), тем самым добавляя дополнительный уровень косвенности и потенциально заставляя будущих читателей искать его определение, теряя непосредственную ясность того, что это unordered_set
.
В этом контексте у меня есть два основных вопроса:
std::tuple
при использовании {}
в качестве аргумента?Мне нужны идеи или альтернативные решения, которые сохранят ясность и простоту кода и при этом правильно устранят предупреждение Clang-Tidy. Любые предложения или объяснения высоко ценятся.
Да, clang-tidy — это правильно. std::tuple
имеет много конструкторов, и поскольку вы используете {}
, конструктор пересылки (который является шаблоном) не может быть вызван.
Можно вызвать только конструктор, принимающий const Types&...
, поскольку вычитание параметра шаблона из {}
не требуется.
Это конструктор (2) на cppreference.
В общем, из {}
нельзя вывести ни один тип, поэтому конструктор пересылки нежизнеспособен. См. Почему автоматический вывод типов шаблонов и шаблонов различается для инициализаторов со скобками?.
Воспроизвести эту проблему можно следующим образом:
#include <tuple>
struct S {
S(const S&);
S(S&&);
};
std::tuple<S, int> foo(S s) {
return std::tuple<S, int>(std::move(s), {});
}
std::tuple<S, int> bar(S s) {
return std::tuple<S, int>(std::move(s), int{});
}
Это компилируется в:
foo(S):
// ...
call S::S(S const&) [complete object constructor]
// ...
bar(S):
// ...
call S::S(S&&) [complete object constructor]
// ...
См. живой пример в Compiler Explorer.
В качестве обходного пути вы должны использовать:
return result_t(std::move(state.inheritingGroups), Result{});
// or
return {std::move(state.inheritingGroups), Result{}};