Search code examples
c++constructor

Copy value for member variable from enclosing space into struct


Consider the following lines of code:

template <typename B>
float cannot_change(float second) {
    B b(second);
    return b();
}

int main() {
    static float global = 10;
    struct C {
        float first_ = global;
        float second_;
        C(float second) : second_(second) {}
        float operator()() { return first_ * second_; }
    };
    std::cout << cannot_change<C>(20) << std::endl;
}

The function cannot_change constructs a template argument B with one parameter. The function is fixed and cannot be changed but I know its source code. Suppose I want to pass a struct that takes two member functions (C) in this example. I have created the struct C which uses the static variable global.

I have a few questions:

  • Is it legitimate to write the struct C as I did or should I refrain from doing this? It seems strange, but I would like to use an element of type B inside cannot_change with two member variables.
  • If this is legitimate, is there another way to set the variable first_ for C than I did? I prefer to copy the variable. Removing the static keywords leads to compile errors.

Solution

  • It looks like what you are trying to do is a best done with a lambda capture, like this:

    
    #include <iostream>
    
    
    int main()
    {
        const float local = 10;
        auto f = [local](float second) { return local * second; };
    
        std::cout << f(20) << std::endl;
        return 0;
    }
    

    You can make a template lambda too if you need to deal with some class's members (so instead of float second, you would write const B & b and use b.member in the function's body), like this:

    #include <iostream>
    
    
    int main()
    {
        const float local = 10;
        struct Something {
           float member1;
           float member2;
        };
        auto f = [local]<typename B>(const B & b) { return local * b.member1 + b.member2; };
    
        std::cout << f(Something{3, 14}) << std::endl;
        return 0;
    }
    

    Godbolt

    If you really need to call cannot_change with this interface, then you can workaround it, like this:

    #include <iostream>
    #include <functional>
    
    
    // You can't change this, so you can't use a lambda directly here
    template <typename B>
    float cannot_change(float second) {
        B b(second);
        return b();
    }
    
    // Here's the function signature of the lambda (and the functor B)
    typedef float (*func_type)(float);
    
    namespace details
    {
        // Here the idea is to capture the lambda in a std::function static instance. 
        // We are not capturing the context of the lambda but the lambda instance itself
        template <typename T>
        struct CaptureLambda {
            // That's the function pointer that'll be used by the Adapter class. 
            // It's instantiated by the compiler on line 35
            static float adapter(float f) {
                // Actually call the std::function => lambda with the "second" parameter of B
                return get()(f);
            };
            // Usual trick to create a static instance of something in a template class
            static std::function<float(float)> & get() { static std::function<float(float)> t; return t; }
            // Set the std::function instance to point to the lambda
            static void set(T & t) { CaptureLambda::get() = t; }
        };
    }
    
    // This is the adapted class that's following the expectation of 
    // "cannot_change" function. It has compile-time captured the CaptureLambda::adapter function
    template <func_type func>
    struct Adapter
    {
        // Store the result of the computation of the lambda
        float result;
        // Simplest solution. You could also compute the result in the () operator, but you'd need to store the parameter
        Adapter(float param) : result(func(param)) {}
        float operator ()() const { return result; }
    };
    int main()
    {
        // Here's a local you want to capture
        const float local = 10;
        // Here's the lambda that's doing the work
        auto f = [local](float second) -> float { return local * second; };
        
        // Because I'm lazy typing long types, create an alias.
        typedef details::CaptureLambda<decltype(f)> CL;
        // Need to capture the lambda instance here
        CL::set(f);
        // And finally call the cannot_change template function
        std::cout << cannot_change<Adapter<CL::adapter>>(20) << std::endl;
        return 0;
    }
    

    Godbolt

    Unlike the comments above, you don't need to store the local global variable in a static variable (with all the trouble it makes).

    Instead, this uses a lambda to capture the local variable. The lambda function instance is captured by a template CaptureLambda structure that's instantiated for this specific lambda. This structure provides a static function with the signature matching the lambda's function signature.

    Finally, the Adapter structure is instantiated with this static function's pointer and implements the interface B that cannot_change is expecting.

    Please notice that:

    1. Since each lambda are different type (even with the same signature), there can't be any conflict on the CaptureLambda static instance that must be set to the same lambda that is used to instantiate it. This means that there is no issue you could find with usual static variable
    2. The Adapter being instantiated for this CaptureLambda specific instance, it can be completely optimized away by the compiler.
    3. I've used a std::function<> in CaptureLambda because I was lazy. A better solution would be to actually copy/move/ref the lambda to avoid the overhead of std::function
    4. If your lambda isn't capturing, don't bother using CaptureLambda and use Adapter<decltype(f)> directly in the cannot_change call, since a non capturing lambda is convertible to a function pointer directly.