2

I was implementing a function in C++, specifically a callable class object, designed to return a std::tuple containing a container of objects and a result. After some adjustments (see below) I have the definition of a member function:

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), {});
}

... where State is:

struct State
{
    std::unordered_set<QnUuid> inheritingGroups{};
    std::unordered_set<QnUuid> visited{};
    std::unordered_set<QnUuid> visiting{};
};

Clang-Tidy gives me warnings on both return statements:

Clang-Tidy: Passing result of std::move() as a const reference argument; no move will actually happen

To resolve this, I explicitly passed Result() in the second return statement:

return result_t(std::move(state.inheritingGroups), Result());

I suspect this might be due to a non-templated constructor of std::tuple being selected when {} is used as one of the arguments. Ideally, I am aiming for a simplified return syntax, such as:

// 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), {}};

However, initializing the tuple with {...} results in compilation issues due to ambiguous constructor calls.

In an attempt to address this, I introduced a typedef alias, but it led to a more verbose and, in my view, less clean version:

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());

I find the introduction of the groups alias to be a suboptimal solution as it only serves to circumvent the Clang-Tidy warning and introduces unnecessary complexity. The alias must be declared outside the immediate scope (State is used by 2 functions), thereby adding an extra layer of indirection and potentially causing future readers to look up its definition, losing the immediate clarity of it being an unordered_set.

With this context, I have two primary questions:

  1. Is Clang-Tidy accurate in stating that the move will not actually occur in this scenario?
  2. Is my suspicion regarding the selection of a non-template constructor for std::tuple when using {} as an argument correct?

I am looking for insights or alternative solutions that maintain code clarity and simplicity while properly addressing the Clang-Tidy warning. Any suggestions or explanations are highly appreciated.

1 Answer 1

1

Yes, clang-tidy is correct. std::tuple has a lot of constructors, and because you're using {}, the forwarding constructor (which is a template) cannot be called. Only the constructor taking const Types&... can be called because no deduction of a template parameter from {} is necessary. This is constructor (2) on cppreference.

In general, no type can be deduced from {}, which is why the forwarding constructor isn't viable. See Why do auto and template type deduction differ for braced initializers?.

You can reproduce this issue as follows:

#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{});
}

This compiles to:

foo(S):
        // ...
        call    S::S(S const&) [complete object constructor]
        // ...
bar(S):
        // ...
        call    S::S(S&&) [complete object constructor]
        // ...

See live example at Compiler Explorer.

As a workaround, you should use:

return result_t(std::move(state.inheritingGroups), Result{});
// or
return {std::move(state.inheritingGroups), Result{}};
Sign up to request clarification or add additional context in comments.

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.