Date: Sun, 18 May 2025 17:38:34 +0100
On Saturday, May 17, 2025, Oliver Hunt wrote:
>
> How would this work?
>
> You’re wanting to call a function, but calling a function requires knowing
> the type of that function, but you cannot know who is calling you yet
> because you haven’t finished compiling the function, let alone the
> functions that might call it.
>
I forgot my laptop today so I'm doing this on my phone, and I'm a tad tired
from running a half-marathon in 2h 7m (PB = 1hr 43m), but hopefully this
works out okay...
First of all I want you to consider the following program:
unsigned a(void) { return 6u; }
unsigned b(void) { return a(); }
unsigned c(void) { return b(); }
int main()
{
return c();
}
When control reaches 'a', if you stop the debugger and do a stack trace,
then you will see that main called c, c called b, and b called a. (But make
sure you turn off all optimisations otherwise 'main' might just get
compiled to two instructions: "mov rax, 0x6" followed by "ret").
In my original post that had the example of:
std::recurse< -1 >( args );
Here I was not at all talking about the function call stack. I was not
talking about what function invokes what function. I was talking about
something very different. I was talking about functions being defined
inside other functions. This was not possible up until C++ 11, when lambdas
were introduced.
I want to make a change to my original idea here. Rather than invoking
"std::recurse" to perform the invocation, instead I want it to return a
function pointer, so I'll rename it to "std::addr_func" which is a
consteval template function defined as follows:
template<unsigned n = 0u>
consteval auto addr_func(void) noexcept
{
// This function returns a function pointer
return __builtin_addr_func(n);
}
If you invoke "std::addr_func" inside a function which is not a lambda
function, then the template parameter 'n' must always be zero, and the
return value will always be a pointer to the current function. You can use
'decltype' on this pointer to figure out the function's return type and
parameter types, as described here:
https://devblogs.microsoft.com/oldnewthing/20200713-00/?p=103978
(I have no affiliation with Microsoft by the way)
Things get interesting though when one function is defined inside another
function:
unsigned Func(unsigned arg)
{
return [](unsigned arg)
{
return [](unsigned arg)
{
return [](unsigned arg)
{
if ( arg % 8u ) return std::addr_func<2u>()( ++arg );
return 4u + arg;
}(arg);
}(arg);
}(arg);
}
int main(void)
{
return Func(2u);
}
In the innermost lambda which contains the statement, "return 4u + arg;",
you can invoke "std::addr_func" four different ways:
std::addr_func<0u>(); -- innermost lambda
std::addr_func<1u>(); -- middle lambda
std::addr_func<2u>(); -- outermost lambda
std::addr_func<3u>(); -- Func
But as you correctly pointed out: When the compiler is processing the
characters of the second lambda, the first lambda isn't finished being
written yet, so the compiler can't possibly know the return type of the
first lambda at this point. So there is a caveat here:
std::addr_func can only be used to get the address of a function whose
return type isn't 'auto' or 'decltype(auto)'. So you'd need to tweak the
outermost lambda as follows:
unsigned Func(unsigned arg)
{
return [](unsigned arg) -> unsigned
{
return [](unsigned arg)
{
return [](unsigned arg)
{
if ( arg % 8u ) return std::addr_func<2u>()( ++arg );
return 4u + arg;
}(arg);
}(arg);
}(arg);
}
But there's something more to consider. What if the function is a member
function? What if it's something like:
unsigned MyClass::Func(unsigned arg)
{
return [](unsigned arg)
{
return [](unsigned arg)
{
return [](unsigned arg)
{
if ( arg % 8u ) return std::addr_func<3u>()( ++arg );
return 4u + arg;
}(arg);
}(arg);
}(arg);
}
Well . . . it would be wonderful and jolly just to say that
'std::addr_func<3u>()' will simply return a member function pointer instead
of a normal function pointer, but then the next question would be "but...
em.... how do you invoke it without a 'this' pointer"? Well here's one
possible solution, and try not to vomit a few centilitres of your last
decaffeinated coffee into your mouth:
unsigned MyClass::Func(unsigned arg)
{
static thread_local MyClass *pthis = nullptr;
pthis = this;
return [](unsigned arg)
{
return [](unsigned arg)
{
return [](unsigned arg)
{
if ( arg % 8u ) return pthis->(*std::addr_func<3u>())(
++arg );
return 4u + arg;
}(arg);
}(arg);
}(arg);
}
Not sure how well that would work out. And furthermore, if the lambdas have
captures then the lambda's addresses would also have to be a 'member
function pointer' instead of a 'function pointer'. We could figure this out
with thread_local variables as I did above, but then we would also need
another new function inside the standard library:
std::this_pointer();
If you invoke the above function inside a lambda (or inside any member
function for that matter), it gives you the lambda's 'this' pointer. (Note
that it does not give you the captured 'this' inside a lambda -- rather it
gives you the actual lambda object's 'this' pointer).
I admit that I'm making this up as I go along. Sometimes half-baked
spontaneous idea sharing is good though.
>
> How would this work?
>
> You’re wanting to call a function, but calling a function requires knowing
> the type of that function, but you cannot know who is calling you yet
> because you haven’t finished compiling the function, let alone the
> functions that might call it.
>
I forgot my laptop today so I'm doing this on my phone, and I'm a tad tired
from running a half-marathon in 2h 7m (PB = 1hr 43m), but hopefully this
works out okay...
First of all I want you to consider the following program:
unsigned a(void) { return 6u; }
unsigned b(void) { return a(); }
unsigned c(void) { return b(); }
int main()
{
return c();
}
When control reaches 'a', if you stop the debugger and do a stack trace,
then you will see that main called c, c called b, and b called a. (But make
sure you turn off all optimisations otherwise 'main' might just get
compiled to two instructions: "mov rax, 0x6" followed by "ret").
In my original post that had the example of:
std::recurse< -1 >( args );
Here I was not at all talking about the function call stack. I was not
talking about what function invokes what function. I was talking about
something very different. I was talking about functions being defined
inside other functions. This was not possible up until C++ 11, when lambdas
were introduced.
I want to make a change to my original idea here. Rather than invoking
"std::recurse" to perform the invocation, instead I want it to return a
function pointer, so I'll rename it to "std::addr_func" which is a
consteval template function defined as follows:
template<unsigned n = 0u>
consteval auto addr_func(void) noexcept
{
// This function returns a function pointer
return __builtin_addr_func(n);
}
If you invoke "std::addr_func" inside a function which is not a lambda
function, then the template parameter 'n' must always be zero, and the
return value will always be a pointer to the current function. You can use
'decltype' on this pointer to figure out the function's return type and
parameter types, as described here:
https://devblogs.microsoft.com/oldnewthing/20200713-00/?p=103978
(I have no affiliation with Microsoft by the way)
Things get interesting though when one function is defined inside another
function:
unsigned Func(unsigned arg)
{
return [](unsigned arg)
{
return [](unsigned arg)
{
return [](unsigned arg)
{
if ( arg % 8u ) return std::addr_func<2u>()( ++arg );
return 4u + arg;
}(arg);
}(arg);
}(arg);
}
int main(void)
{
return Func(2u);
}
In the innermost lambda which contains the statement, "return 4u + arg;",
you can invoke "std::addr_func" four different ways:
std::addr_func<0u>(); -- innermost lambda
std::addr_func<1u>(); -- middle lambda
std::addr_func<2u>(); -- outermost lambda
std::addr_func<3u>(); -- Func
But as you correctly pointed out: When the compiler is processing the
characters of the second lambda, the first lambda isn't finished being
written yet, so the compiler can't possibly know the return type of the
first lambda at this point. So there is a caveat here:
std::addr_func can only be used to get the address of a function whose
return type isn't 'auto' or 'decltype(auto)'. So you'd need to tweak the
outermost lambda as follows:
unsigned Func(unsigned arg)
{
return [](unsigned arg) -> unsigned
{
return [](unsigned arg)
{
return [](unsigned arg)
{
if ( arg % 8u ) return std::addr_func<2u>()( ++arg );
return 4u + arg;
}(arg);
}(arg);
}(arg);
}
But there's something more to consider. What if the function is a member
function? What if it's something like:
unsigned MyClass::Func(unsigned arg)
{
return [](unsigned arg)
{
return [](unsigned arg)
{
return [](unsigned arg)
{
if ( arg % 8u ) return std::addr_func<3u>()( ++arg );
return 4u + arg;
}(arg);
}(arg);
}(arg);
}
Well . . . it would be wonderful and jolly just to say that
'std::addr_func<3u>()' will simply return a member function pointer instead
of a normal function pointer, but then the next question would be "but...
em.... how do you invoke it without a 'this' pointer"? Well here's one
possible solution, and try not to vomit a few centilitres of your last
decaffeinated coffee into your mouth:
unsigned MyClass::Func(unsigned arg)
{
static thread_local MyClass *pthis = nullptr;
pthis = this;
return [](unsigned arg)
{
return [](unsigned arg)
{
return [](unsigned arg)
{
if ( arg % 8u ) return pthis->(*std::addr_func<3u>())(
++arg );
return 4u + arg;
}(arg);
}(arg);
}(arg);
}
Not sure how well that would work out. And furthermore, if the lambdas have
captures then the lambda's addresses would also have to be a 'member
function pointer' instead of a 'function pointer'. We could figure this out
with thread_local variables as I did above, but then we would also need
another new function inside the standard library:
std::this_pointer();
If you invoke the above function inside a lambda (or inside any member
function for that matter), it gives you the lambda's 'this' pointer. (Note
that it does not give you the captured 'this' inside a lambda -- rather it
gives you the actual lambda object's 'this' pointer).
I admit that I'm making this up as I go along. Sometimes half-baked
spontaneous idea sharing is good though.
Received on 2025-05-18 16:38:37