Reputation: 1187
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
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 f
s different contexts in the terse code adds to the confusion.
Upvotes: 1
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
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