Search code examples
c++if-constexpr

if constexpr to initiallize a const reference


I have an expensive operation that I want to call conditionally depending on the template type. The naive function looks basically like this.

template <typename T>
void FooNaive( const std::vector<ExpensiveStruct<T>>& vec )
{
    std::vector<ExpensiveStruct<float>> vecAsFloat;
    if constexpr ( std::is_same_v<T, float> )
        vecAsFloat = vec;   // expensive copy I want to avoid
    else
    {
        vecAsFloat.reserve( vec.size() );
        for ( const auto& s : vec )
            vecAsFloat.push_back( ConvertExpensiveStruct( s ) );   // <-- does not compile for floats
    }

    // ...use vecAsFloat...
}

In order to avoid the expensive copy of the first branch, I tried making vecAsFloat a const reference. However, due to ConvertExpensiveStruct not compiling for float types (second branch), I still need an if constexpr in there.

This is my current attempt at it:

template <typename T>
void FooAttempt( const std::vector<ExpensiveStruct<T>>& vec )
{
    std::vector<ExpensiveStruct<float>> dummyVecAsFloat;
    if constexpr ( !std::is_same_v<T, float> )
    {
        dummyVecAsFloat.reserve( vec.size() );
        for ( const auto& s : vec )
            dummyVecAsFloat.push_back( ConvertExpensiveStruct( s ) );
    }
    const std::vector<ExpensiveStruct<float>>& vecAsFloat = std::is_same_v<T, float> ? vec : dummyVecAsFloat;

    // use vecAsFloat...
}

I also thought about putting the conversion loop in a lambda to be called in-place inside the ternary operator, but that would mean it would have to return a const reference to its local variable (extends lifetime, but still ugly), or at worse return a copy by value (do lambdas have NRVO?).

But it looks overly complicated. Is there a way to write this simpler, or at least with clearer intent?


Solution

  • You can extract whatever you do at // use vecAsFloat... into a separate function, and then use if constexpr to create a temporary std::vector<ExpensiveStruct<float>> only if needed:

    void FooAsFloat(const std::vector<ExpensiveStruct<float>>& vecAsFloat) {
        // use vecAsFloat...
    }
    
    template <typename T>
    void Foo(const std::vector<ExpensiveStruct<T>>& vec)
    {
        if constexpr (std::is_same_v<T, float>) {
            FooAsFloat(vec);  // no copy needed
        }
        else {
            std::vector<ExpensiveStruct<float>> vecAsFloat;
            vecAsFloat.reserve(vec.size());
            for (const auto& s : vec) {
                vecAsFloat.push_back(ConvertExpensiveStruct(s));
            }
            FooAsFloat(vecAsFloat); // call with copy
        }
    }
    

    As @Jarod42 commented below, you can even avoid the extration into another function:
    Do the main processing in the if part, and use a recursive call for the else part after creating the needed std::vector<ExpensiveStruct<float>>:

    template <typename T>
    void Foo(const std::vector<ExpensiveStruct<T>>& vec)
    {
        if constexpr (std::is_same_v<T, float>) {
            // use vec (it contains `ExpensiveStruct<float>` elements as needed) 
            // ...
        }
        else {
            std::vector<ExpensiveStruct<float>> vecAsFloat;
            vecAsFloat.reserve(vec.size());
            for (const auto& s : vec) {
                vecAsFloat.push_back(ConvertExpensiveStruct(s));
            }
            // Recursive call with `vecAsFloat`
            // (when executed it will get into the main `if constexpr` part to be used):
            Foo(vecAsFloat);
        }
    }