-
-
Notifications
You must be signed in to change notification settings - Fork 417
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support etl::inplace_function #1046
base: master
Are you sure you want to change the base?
Support etl::inplace_function #1046
Conversation
using dtor_func_t = void (*)(storage_ptr_t); | ||
|
||
const invoke_func_t invoke_func{[](storage_ptr_t, Args&&...) -> R | ||
{ return R(); }}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
std::function
throws std::bad_function_call
if operator() is called on an empty object while etl::inplace_function
safely returns a default-constructed object in this case.
That means type typename R
is required to be default-constructible.
void
type is fine here ( https://timsong-cpp.github.io/cppwp/std14/expr.type.conv )
But this will not compile since std::is_default_constructible_v<Type_a>
is false:
struct Type_a
{
Type_a(int32_t val) : val_(val) {}
int32_t val_;
};
etl::inplace_function<Type_a()> func0;
This deviation from std::function
is by designed and also tested in test_inplace_function.cpp
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In etl::delegate
an ETL error is raised.
ETL_ASSERT(is_valid(), ETL_ERROR(delegate_uninitialised));
Also in etl::delegate
there are call_if
and call_or
member functions that handle uninitialised objects.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Updated to make it raise etl::bad_inplace_function_call
when an empty etl::inplace_function
is invoked with operator()
. (Similar to std::bad_function_call
)
std::function
does not support call_if()
and call_or()
. Do you suggest adding these 2 member func?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@jiangyilism I suggest adding them. It avoids having to check if the object is assigned anything, and helps moving from delegate
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
clear()
would also be nice to have for delegate
compatibility.
https://github.com/ETLCPP/etl | ||
https://www.etlcpp.com | ||
|
||
Copyright(c) 2025 BMW AG |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
How close a copy is this of the implementations that 'inspired' you?
There may be copyright implications.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The closest open source implementation is SG14 inplace_function.
etl::inplace_function
compiled object memory layout will be almost identical (except alignment).
The main differences:
- etl is compatible with c++11 while others, including SG14, typically requires c++14
- etl one has no member/non-member function
swap()
,operator==(nullptr_t)
,operator!=(nullptr_t)
. - etl does not utilize aligned storage, which will be deprecated in c++23.
- etl renames most of the class and variable names, except the name "inplace_function", which can be renamed too.
- etl simplifies some template meta programming with etl type_traits header
- move almost all stuffs in private namespace into private sections of classes.
typename T, | ||
typename C = etl::decay_t<T>, | ||
typename = etl::enable_if_t< | ||
!private_inplace_function::is_inplace_function<C>::value && etl::is_invocable_r<R, C&, Args...>::value>> |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This class has limited usefulness as is restricted to STL and C++14 and above.
Many of the users of the ETL do not/cannot use the STL.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
etl::is_invocable_r
depends on ETL_USING_CPP11
but not STL.
I updated to make etl::inplace_function
compatible to c++11.
af095d7
to
064f21d
Compare
The missing R-value support blocks us from upgrading ETL. This will be great :) |
064f21d
to
2a9fb5d
Compare
explicit ETL_CONSTEXPR vtable_t(etl::type_identity<T>) ETL_NOEXCEPT | ||
: invoke_func{[](char* pobj, Args&&... args) -> R | ||
{ return (*reinterpret_cast<T*>(pobj))( | ||
etl::forward<Args&&>(args)...); }}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I'm confused about this construction. Why etl::forward<Args&&>
instead of etl::forward<Args>
is used here?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks. Fixed to etl::forward<Args>
.
ETL_ASSERT(vtable_ptr->invoke_func, ETL_ERROR(bad_inplace_function_call)); | ||
return vtable_ptr->invoke_func( | ||
obj, | ||
etl::forward<Args>(args)...); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does conversion from Args...
to Args&&...
is working here?
For example I want to pass some struct Foo
by value into inplace_function. Would it be copied three times?
First in call to this operator()
, second in invoke_func
call, and third in call to actual functor inside the invoke_func
lambda?
I'm not an expert I'm just wondering how this should work.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe std::move(args)
should be used here, and then std::forward<Args>(args)
in invoke_func
lambda?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It is copied twice under gcc/clang -O0 , -O1 and -O2 . Using etl::move
/std::move
still copies twice.
A test case NoRedundentCopy
is added to verify that during inplace_function::operator()
, if the contained callable signature is:
- call-by-value: then at most 2 copyings are made in
inplace_function::operator()
. - call-by-reference: then no copying is made.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This example gives me one copy and one move https://godbolt.org/z/3YPMccbnz
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yes. And if the move constructor is not explicitly defined then copy constructor is called instead. NoRedundentCopy
only checks the number of copy is less than 3 . It roughly checks that inplace_function is not copying too many times than expected. It does not guarantee the exact number of copying.
With gcc:
- copy ctor not explicitly defined : copy twice
- copy ctor =default : copy once
- copy ctor explicitly defined : copy once
Out of curiosity, what's using all the space? I had to bump the size to 48 bytes for an |
Depends on what you save inside. If it is just a "void f1(void*){}" it maybe fits? But if it's a lambda capturing a number of arguments by value inside, it may be much more. |
John, can you please enable the CI workflows for this PR? |
Actually, I accidentally compiled for host so it's 48 bytes on a 64 bit system which makes a bit more sense. |
What if ETL_INPLACE_FUNCTION_DEFAULT_CAPACITY would be calculated based on pointer size, like |
#include "../utility.h" | ||
|
||
#ifndef ETL_INPLACE_FUNCTION_DEFAULT_CAPACITY | ||
#define ETL_INPLACE_FUNCTION_DEFAULT_CAPACITY 32 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can this be based on the target architecture? When building for 64 bit the bare minimum (void()
) seems to be 40 bytes.
Similar to etl::delegate and etl::function, inplace_function has one single global size. This one needs to store all possible closures in a whole project that uses ETL for a inplace_function template instance type. I.e. it depends. E.g. for a simple etl::inplace_function<void(void *)> as mentioned by Denravonska, you might save different closure implementations, e.g.:
I.e. needs to be adjusted if necessary for every project, to not waste memory. I'm not sure if you can find a way to automatically calculate a global maximum size over all user locations. Finally, you can only find at link time (or after compiling all comile units) if you have captured all necessary source locations. |
I agree with that, and it's the library user's responsibility to use only closures that fit or provide a large enough value to But why did 32 was chosen for default? For me the usual case closure captures some values by reference i.e. maximum 4 pointers for 32 byte storage on 64 bit system. @denravonska also asked about ETL_INPLACE_FUNCTION_DEFAULT_CAPACITY based on architecture. I think solution based on |
Hi @BartolomeyKant and @denravonska , I think now I got your point. :-) And I agree that different defaults for e.g. 32 and 64 bit platforms can be provided. Even though an individual ETL_INPLACE_FUNCTION_DEFAULT_CAPACITY will typically be part of a project's etl_profile.h anyway. The original default of 32 covered our initial use cases in a 32 bit project. Luckily, the compiler will tell you at build time if it doesn't fit, as shown in @denravonska 's example. |
I thought about this a bit more. My original concern was that the default size was valid for 32 bit but failed to compile on 64 bit, so maybe the default should be the smallest size required to fit the simplest inplace function given the target architecture. However this only applies to the default function. As soon as you do anything fancier you need to handle the size differences in the application anyway. |
Thanks for all the feedback. |
He also said
I have no major quarrel with the implementation. The only request I have so far is a default size that works for both 32- and 64 bit systems. |
@jiangyilism No, keep it active for the time being. I'm in Greece at the moment, so I'll look at this functionality again when I get back next week. |
2a9fb5d
to
a8c9a54
Compare
@denravonska , @BartolomeyKant |
a8c9a54
to
2362d9d
Compare
test/test_inplace_function.cpp
Outdated
{ | ||
SUITE(test_inplace_function) | ||
{ | ||
TEST(NoRedundentCopy) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
NoRedundantCopy
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It would be a quality of life change with .call_if()
. Consider
for(auto cb : callbacks) {
if(cb) {
cb();
}
}
vs
for(auto cb : callbacks) {
cb.call_if();
}
That, .call_or
and a .clear
would make it a lot more compatible with delegate
.
Provide similar api to std::function. Store a callable inside the inplace_function object's member field in a type-erasure way. The member field, i.e. storage or buffer, has a compile-time fixed size. The size is specify either by the macro ETL_INPLACE_FUNCTION_DEFAULT_CAPACITY or a non-type template parameter. The implementation is inspired by: 1. SG14 inplace_function 2. folly::Function 3. function2
2362d9d
to
d5726a9
Compare
std::function does not support call_if, call_or, clear . If we want all etl::delegate api then it might be better to make inplace_function and delegate into a same class.
|
I'm thinking more from the convenience perspective. I think a very common use case is going to be moving from I guess it all boils down to how true to the standard ETL wants to be in terms of adding functionality that's outside of the specification. |
I'd propose @jiangyilism 's option 1. in this way: Be close to std::function to minimize confusion, and if possible rename etl::inplace_function to etl::function. The latter of course only if @jwellbelove decides to abandon the old (already deprecated) etl::function. |
@rolandreichweinbmw The current I don't have any problem with adding functions that enhance the usability of an ETL class; making an exact clone of the STL was never my intention. |
I've always treated the STL as inspiration for classes in the ETL, not as a rigid specification for what can and can't be included. |
I have limited options to review this PR at the moment, as I'm on holiday in Kos. I'm not quite sure when I will get back, as the Greek air traffic controllers are threatening a strike on the day we are supposed to fly home, and the postponed flights may be backed up for a day or two. |
Provide similar api to std::function.
Current use case is mainly to hold a r-value lambda object, which etl::delegate is unsuitable since etl::delegate does not extend the lifetime of r-value lambda object.
Store a callable inside the inplace_function object's member field in a type-erasure way.
The member field, i.e. storage or buffer, has a compile-time fixed size.
The size is specified either by the macro ETL_INPLACE_FUNCTION_DEFAULT_CAPACITY or a non-type template parameter.
The implementation is inspired by: