Simple macro to allow 'constexpr' generic no-capture lambdas in C++14
{ Written by Aaron McDaid - [email protected] }
In C++14, lambdas are very useful but they have some restrictions. They can't be used in certain contexts and they aren't very friendly with constexpr.
{ Update: Breaking change pushed on 28th December 2017 - fewer commas required now, simpler interface }
For example, the following code works in C++17, but not C++14 (error: call to non-constexpr function 'main()::<lambda(int)>'):
constexpr int a =
[](int x){return x*x;} // a lambda
(4) // call the lambda
;
static_assert(a == 16 ,"");
With this library, you can write this in C++14:
constexpr auto a =
CONSTEXPR_LAMBDA(x)( return x*x; ) // a macro created a constexpr pseudo-lambda
(4); // call this pseudo-lambda
static_assert(a == 16 ,"");
Within the first set of parentheses, you specify a 'capture type' (by &-reference, by value, or by && forwarding reference) as well as a name for each parameter.
This essentially creates a generic lambda, i.e. as if all parameters are auto or auto & or auto &&.
For example:
CONSTEXPR_LAMBDA(&&a,&&b) ....// capture two args called 'a' and 'b', both of them by forwarding reference
CONSTEXPR_LAMBDA( &x, y) ....// capture two args called 'x' and 'y', 'x' is by reference, 'y' by value
// the above are essentially equivalent to:
(auto && a , auto && b)
(auto & x , auto y)
You can even use the last parameter to capture a pack, with CONSTEXPR_LAMBDApack instead of CONSTEXPR_LAMBDA, i.e. CONSTEXPR_LAMBDApack(first_arg, second_arg, pack), but I can't get it to work on MSVC (tested on gcc.godbolt.org).
Here is a more complicated demo showing the reference capture:
constexpr auto
test_reference_capture()
{
int A = 10;
int B = 100;
int C = 1000;
int product = CONSTEXPR_LAMBDA(&a,b,c)
(
int product = a*b*c;
a=2; // 'a' was captured by reference
b=0; // 'b' and 'c' were captured by value,
c=0; // so these assignments don't realy do anything
return product;
)
(A,B,C);
// 'product' should be 1000000 now
// 'A' was captured by reference, and hence is now 2
return A + B + C + product;
}
static_assert(test_reference_capture() == 2 + 100 + 1000 + 1000000 ,"");
Through testing on gcc.godbolt.org, this works on gcc >= 5.1 and clang >= 3.6. Just remember to use -std=c++14. It also works with the version of MSVC' currently on gcc.godbolt.org, "MSVC 19 2017 RTW".
However, I can't get packs to work with MSVC.
I think this is standard C++14, and if not I think we can fix it if necessary. So don't hesitate to send me any
improvements!
I haven't given any serious thought to how to implement capturing-lambdas here, but I think it should be easy enough to do so. However, if you really need such a thing in a particular context, maybe you should just write your own class out-of-line!
To begin, you would write a new class with the appropriate call operator:
struct x {
constexpr auto
operator() (int x)
{ return x * x; }
};
But this can't (usually) be written in just any location, for example you can't define new types
in the middle of expression.
In order to work around this, and define a new type in the middle of an existing expression,
we create a lambda which
returns this x 'type' indirectly via a pointer. This allows us to write our type anywhere
we want: (except in unevaluated contexts, for which you should consider my {crazy!} cambda library)
auto * ptr =
[](){
struct x {
constexpr auto
operator() (int x)
{ return x * x; }
};
return (x*)nullptr;
} // define a lambda
(); // call it, returning the nullptr pointer-to-x
But we want a generic function call operator. This is not yet generic as the argument is fixed to be int.
Unfortunately, we can't write templates inside these local classes. i.e. this isn't allowed.
auto * ptr =
[](){
struct x {
template<typename T> // templates not allowed in local classes
constexpr auto
operator() (int x)
{ return x * x; }
};
return (x*)nullptr;
} // define a lambda
(); // call it, returning the nullptr pointer-to-x
... so we use the outer lambda to 'encode' the type of the parameter
auto funny_lambda_returning_our_pseudo_lambda =
[](auto outer_x){ /* this 'outer_x' is never really used as a value, its
* purpose is just to encode a type. */
struct x {
constexpr auto
operator() (decltype(outer_x) x)
{ return x * x; }
};
return (x*)nullptr;
};
Now, we can extract an instance of 'x' for the correct argument type:
using x_int = std::decay_t<decltype(*funny_lambda_returning_our_pseudo_lambda(std::declval<int>()))>;
static_assert(std::is_same< int , decltype(x_int{}(3)) >{} ,"");
using x_double = std::decay_t<decltype(*funny_lambda_returning_our_pseudo_lambda(std::declval<double>()))>;
static_assert(std::is_same< double , decltype(x_double{}(3)) >{} ,"");
// next, construct and call these two objects. The second one is
// just to confirm that it truly is the int-to-int version
static_assert(2.25 == x_double{} (1.5) ,"");
static_assert(1 == x_int {} (1.5) ,"");
The call_forwarder class template automates this for us, generically for any number
and type of arguments. The arguments passed by the user are used to lookup the appropriate
inner class X and then perfectly-forward the arguments into the call operator inside X.
L is the type of the generic lambda (essentially, decltype(funny_lambda_returning_our_pseudo_lambda):
template<typename L>
struct call_forwarder
{
template<typename ... T>
constexpr auto
operator() (T && ... t)
->decltype(auto)
{
using X = std::decay_t<decltype( *std::declval<L&>()(std::forward<T>(t)...) )>;
// X is the nested 'CONSTEXPR_LAMBDA_arbitrary_hidden_struct_namelkdsjflkafdlksafdja' type
return X{} (std::forward<T>(t)...);
}
};
We therefore need a function to construct a call_forwarder<L>. That's make_CONSTEXPR_LAMBDA as called here,
where null_address_of is a small helper function to return a null pointer pointing to the type of its argument.
In other words, null_address_of(3.5) gives us (double*)nullptr.
auto pseudo_lambda =
make_CONSTEXPR_LAMBDA(
null_address_of(
[](auto && arg0) {
struct x
{
constexpr auto
operator() (decltype(arg0)&& arg0)
->decltype(auto)
{
return arg0*arg0;
}
};
return (x*) nullptr;
}
)
);
The final problem is that is isn't itself a constant expression. The lambda itself, [](){...}, isn't a constant
expression and therefore this pervades the entire expression, resulting in a non-constant pseudo_lambda.
This is unfortunate as we know the return value of null_address_of( ) is exactly nullptr - how can
we 'force' it to be constexpr?
In other words, we can do this:
constexpr auto * p1 = (int*) nullptr;
but not:
constexpr auto * p1 = null_address_of(3); // error as the entire expression isn't a constant expression
We can resolve this with the "?:" trick:
constexpr auto * p1 = true ? nullptr : null_address_of(3); // this works
Normally, if any argument to a function is non-constant, the entire expression is non-constant. But there is
an exception here for this ?: operator. As the boolean condition (true) is a constant, the value is
taken directly from in between the ? and :, and it ignores the 'non-constant-ness' of the null_address_of(3)
after the :. This means that the entire expression is a constant expression.
The 'value' of null_address_of(3) is ignored, in fact it is never even evaluated, but it is not
entirely useless as it is used to control the type.
This finally gives us a way to construct the pseudo_lambda that we desire:
auto pseudo_lambda = make_CONSTEXPR_LAMBDA(true ? nullptr : null_address_of([](auto && arg0) {
struct x
{
constexpr auto
operator() (decltype(arg0)&& arg0)
->decltype(auto)
{
return arg0*arg0;
}
};
return (x*) nullptr;
}));
static_assert(pseudo_lambda(4) == 16 ,"");
The header, CONSTEXPR_LAMBDA.hh, includes implementations of null_address_of, struct call_forwarder, and make_CONSTEXPR_LAMBDA
and a macro CONSTEXPR_LAMBDA to detect the number of arguments and generate the boilerplate.