Acorn
Acorn

Reputation: 1187

std::forward and operator()

I've been looking into writing a static_if for my C++ project, and I stumbled across the following piece of code:

#include <iostream>
using namespace std;

namespace static_if_detail {

struct identity {
    template<typename T>
    T operator()(T&& x) const {
        return std::forward<T>(x);
    }
};

template<bool Cond>
struct statement {
    template<typename F>
    void then(const F& f){
        f(identity());
    }

    template<typename F>
    void else_(const F&){}
};

template<>
struct statement<false> {
    template<typename F>
    void then(const F&){}

    template<typename F>
    void else_(const F& f){
        f(identity());
    }
};

} //end of namespace static_if_detail

template<bool Cond, typename F>
static_if_detail::statement<Cond> static_if(F const& f){
    static_if_detail::statement<Cond> if_;
    if_.then(f);
    return if_;
}

template<typename T>
void decrement_kindof(T& value){
    static_if<std::is_same<std::string, T>::value>([&](auto f){
        f(value).pop_back();
    }).else_([&](auto f){
        --f(value);
    });
}


int main() {
    // your code goes here
    std::string myString{"Hello world"};
    decrement_kindof(myString);
    std::cout << myString << std::endl;
    return 0;
}

It all makes sense to me, except for one thing: the overloaded operator() in struct identity. It takes in a rhs of type T called x, cool and all. But when identity is called, nothing is actually passed into identity.

template<typename F>
void then(const F& f){
    f(identity());
}

Above, f calls identity, but passes nothing onto identity. Yet identity returns the forwarded arguments (in my case, an std::string), and pops the backmost character of the string. How is identity returning a forwarded argument, when itself has no arguments passed into it to forward?

Upvotes: 3

Views: 292

Answers (3)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275800

template<typename F>
void then(const F& f){
  f(identity());
}

Is more readable as

template<typename F>
void then(const F& f){
  f(identity{});
}

they are construcing an identity object, not calling one.

The trick here is that the non-dependent parts of a template function must be valid even if the function is never instantiated.

So saying value.pop_back() is never valid within the lambda when value is an integer.

By passing identity{} to exactly one of the then or else cases, we can avoid this problem.

The statement f(value) produces a dependent type. So it need only be valid when the template operator() of the lambda is actually instantiated (there must also be some possibke f that makes it valid, but that is a corner case).

As we only instantiate the path the condition tells us to, the f(value) can be used almost any way we want, so long as it is valid in the taken branch.

I would have called f a better name, like safe or guard or var or magic rather than f. The use of two fs different contexts in the terse code adds to the confusion.

Upvotes: 1

Barry
Barry

Reputation: 303537

f doesn't call identity - f is called with an instance of identity. Walking through the two cases here:

static_if<std::is_same<std::string, T>::value>([&](auto f){
    f(value).pop_back();
}).else_([&](auto f){
    --f(value);
});

If T is std::string, then we instantiate a statement<true> whose then() invokes the passed-in function with an instance of identity. The argument to the first lambda, f, will be of type identity - so f(value) is really just value and we do value.pop_back().

If T is not std::string, then we instantiate a statement<false> whose then() does nothing, and whose else_() invokes the lambda with an instance of identity. Again f(value) is just value and we do --value.


This is a really confusing implementation of static_if, since f in the lambda is always an identity. It's necessary to do because we can't use value directly (can't write value.pop_back() since there's no dependent name there so the compiler will happily determine that it's ill-formed for integers), so we're just wrapping all uses of value in a dependent function object to delay that instantiation (f(value) is dependent on f, so can't be instantiated until f is provided - which won't happen if the function isn't called).

It would be better to implement it so that you actually pass in the arguments to the lambda.

Upvotes: 5

WhiZTiM
WhiZTiM

Reputation: 21576

Let us take the case, where your Cond is true in the static_if, hence, the primary template class will be used...

template<bool Cond>
struct statement {
    template<typename F>
    void then(const F& f){
        f(identity());
    }

    template<typename F>
    void else_(const F&){}
};

Recall, that your calling function is:

static_if<std::is_same<std::string, T>::value>
(
   [&](auto f){ //This is the lamda passed, it's a generic lambda
        f(value).pop_back();
    }
).else_(
   [&](auto f){
        --f(value);
    }
);

In the applying function below, F is a type of that generic lambda (meaning, you can call f with any type)

template<typename F>
void then(const F& f){
    f(identity());
}

identity() creates an object of type identity which is then passed as an argument, to call your generic lambda.

 [&](auto f){ //This is the lamda passed, it's a generic lambda
        f(value).pop_back();
    }

but recall, f is an object of type identity and has a templated call () operator, which basically returns the object passed to it.


So, we go like this:

void decrement_kindof(std::string& value){
    static_if<std::is_same<std::string, std::string>::value>([&](auto f){
        f(value).pop_back();
    }).else_([&](auto f){
        --f(value);
    });
});

Reduced to:

void decrement_kindof(std::string& value){
    static_if<true>([&](auto f){
        f(value).pop_back();
    }).else_([&](auto f){
        --f(value);
    });
});

Reduced to:

void decrement_kindof(std::string& value){
    static_if<true>(
        [&](identity ident){
             auto&& x = ident(value); //x is std::string()
             x.pop_back();
       } (identity())  //<-- the lambda is called

    ).else_(
        [&](auto f){    //This is not evaluated, because it's not called by the primary template of `statement`
            --f(value);
        }
    );   
});

Upvotes: 0

Related Questions