Home > database >  C lambda macro to force argument type
C lambda macro to force argument type

Time:07-15

Going through cdda's opensource codebase I found a pretty funky looking macro that I'd like to understand better

Macro definition

/**
*  The purpose of this macro is to provide a concise syntax for
*  creation of inline literals (std::string, string_id, etc) that
*  also have static scope.
*
*  Usage:
*    instead of:  some_method( "string_constant" );
*    use:         some_method( STATIC ( "string_constant" ) );
*
*    instead of:  some_method( string_id<T>( "id_constant" ) );
*    use:         some_method( STATIC ( string_id<T>( "id_constant" ) ) );
*
*    const V &v = STATIC( expr );
*    // is an equivalent of:
*    static const V v = expr;
*
*    Note: `expr` being char* (i.e. "a string literal") is a special case:
*      for such type of argument,  STATIC(expr) has `const std::string &` result type.
*      This is to support expressions like STATIC("...") instead of STATIC(std::string("..."))
*/

The Macro with my comments trying to clarify what each bit is doing

//Forces a type 
template<typename T>
inline const T &static_argument_identity( const T &t ){
    return t;
}

#define STATIC(expr) \
    (\
        ([]()-> const auto &{ \
                /*Get the type and convert to typedef */ \
                using ExprType = std::decay_t<decltype(static_argument_identity( expr ))>; \
\
                /*Specail logic to make string literals std::strings, else leave it alone */ \
                using CachedType = std::conditional_t<std::is_same<ExprType, const char*>::value, std::string, ExprType>; \
\
                /*Reassign the type */ \
                static const CachedType _cached_expr = static_argument_identity( expr ); \
\
                return _cached_expr; \
            }\
        )()\
    )

So if we were to use the int 5, this would equate to:

[]()-> const auto&{return const int&(5)}()

And if we use "Hello", this would equate to

[]()-> const auto&{return const std::String&("Hello")}()

Questions

Why does static_argument_identity need to be used in:

using ExprType = std::decay_t<decltype(static_argument_identity( expr ))>; 

Why doesn't this throw? Does this do implicit conversion from "Hello" to std::String("Hello")

const std::String &static_argument_identity( const std::string &t ){
    return t;
}

Why does the lambda have the "()" after it? Is the execute call for the function?

-> const auto&{return ...}()

And finally would the compiler compile all these out to just constant variables, since it's a pretty straight forward conversion?

CodePudding user response:

OP asked

Why does the lamda have the "()" after it? Is the execute call for the function?

Yes. This is the definition of a lambda which is immediately called. Hence, the whole forms an expression and can be used where expressions (of compatible type) are expected.

Nevertheless, the lambda's body can contain everything what's allowed in every function body i.e. declarations, definitions, and statements.


The expanded sample code for 5 provided by OP is not quite correct. More correct would be:

[]() -> const auto& {
  static const int _cached_expr = 5;
  return _cached_expr;
}()

This leads to the question: What is the advantage of storing a literal in a static variable which lives until end of process?

If a value is provided where a const reference is expected, the value will be wrapped into a temporary. For repeated calls this will be a new temporary for each invocation.

It's not clear to me under which conditions this might be undesired. However, the STATIC macro with its lambda magic turns a value into a static variable so that a const reference to the always same instance can be provided.

It's a bit difficult to illustrate this with a plain int literal. Hence, I used a sample struct with an unique id to be able to distinguish instances.

The effect of STATIC illustrated in an MCVE (on coliru):

#include <iostream>

//Forces a type 
template<typename T>
inline const T &static_argument_identity( const T &t ){
    return t;
}

#define STATIC(expr) \
    (\
        ([]()-> const auto &{ \
                /*Get the type and convert to typedef */ \
                using ExprType = std::decay_t<decltype(static_argument_identity( expr ))>; \
\
                /*Specail logic to make string literals std::strings, else leave it alone */ \
                using CachedType = std::conditional_t<std::is_same<ExprType, const char*>::value, std::string, ExprType>; \
\
                /*Reassign the type */ \
                static const CachedType _cached_expr = static_argument_identity( expr ); \
\
                return _cached_expr; \
            }\
        )()\
    )

struct Value {
  static inline unsigned id = 0;
  const int value;

  Value(int value): value(value) {   id; }
};

std::ostream& operator<<(std::ostream& out, const Value& value)
{
  return out << "Value { " << value.value << " } #" << value.id;
}

void f(const Value& value)
{
  std::cout << "f(" << value << ")\n";
}

#define DEBUG(...) std::cout << #__VA_ARGS__ << ";\n"; __VA_ARGS__ ; std::cout << '\n'

int main()
{
  DEBUG(for (int i = 0; i < 3;   i) f(Value(123)));
  DEBUG(for (int i = 0; i < 3;   i) f(STATIC(Value(123))));
}

Output:

for (int i = 0; i < 3;   i) f(Value(123));
f(Value { 123 } #1)
f(Value { 123 } #2)
f(Value { 123 } #3)

for (int i = 0; i < 3;   i) f(STATIC(Value(123)));
f(Value { 123 } #4)
f(Value { 123 } #4)
f(Value { 123 } #4)

In the first loop, each call of f() is invoked with a new instance of Value.

In opposition, in the second loop f() is always invoked with the same instance of Value. Hence, it's not only an equal object — it's the exact same object which is "received" in f().

Unfortunately, I must admit that I'm not aware where this difference might be important although I remember another question (this or last year) where this difference was required.


Concerning

template<typename T>
inline const T &static_argument_identity( const T &t ){
    return t;
}

I assume that it's used to turn (the type of) an expression into a const reference. If it already was a const reference it stays a const reference. If it was not a const reference it becomes a const reference.

For the sample of OP:

5 has the type int.

Hence, for static_argument_identity(5), the compile deduces:

inline const int& static_argument_identity(const int& t) { return t; }

and decltype(static_argument_identity( expr )) will return const int&.

std::decay removes all cv qualifiers and the reference to unwrap the inner type.

I must admit that I'm not that confident in template programming to explain why it's necessary to turn something into a const reference (for sure) to unwrap the basic type afterwards.

std::decay_t<decltype(expr)>

should have the same effect — but maybe only for certain types but not all.

  • Related