Skip to content

Conversation

@cmazakas
Copy link
Member

Resolves: #23

Right now the invoke holders take the storage by-value which is strictly incorrect when using SBO as it creates a copy of the callable which leads to surprising results in user-code that expect multiple calls to a lambda with a mutable capture.

@pdimov
Copy link
Member

pdimov commented Jan 16, 2026

Instead of storage&, make it a template Storage& to capture the const which will allow you to omit the const_casts.

@cmazakas
Copy link
Member Author

Instead of storage&, make it a template Storage& to capture the const which will allow you to omit the const_casts.

I'm trying that but I'm having trouble getting code like this to then compile:

        move_only_function<int( noex_callable ) const> f2( std::move( f1 ) );
        BOOST_TEST_EQ( f2( c ), 1235 );

        move_only_function<int( noex_callable )> f3( std::move( f2 ) );
        BOOST_TEST_EQ( f3( c ), 1235 );

The compilers reject it with:

./boost/compat/move_only_function.hpp:421:24: error: invalid conversion from ‘int (*)(boost::compat::detail::move_only_function_base<boost::compat::detail::ref_quals::none, true, false, int, noex_callable>::storage_type&, noex_callable&&)’ {aka ‘int (*)(const boost::compat::detail::storage&, noex_callable&&)’} to ‘int (*)(boost::compat::detail::move_only_function_base<boost::compat::detail::ref_quals::none, false, false, int, noex_callable>::storage_type&, noex_callable&&)’ {aka ‘int (*)(boost::compat::detail::storage&, noex_callable&&)’} [-fpermissive]
  421 |         invoke_ = base.invoke_;

The problem being there's no conversion from:

int (*)(const boost::compat::detail::storage&

to

int (*)(boost::compat::detail::storage&

Many of the tests fail like:

libs/compat/test/move_only_function_test.cpp:844:41: note: in instantiation of function template specialization 'boost::compat::move_only_function<int (long)>::move_only_function<boost::compat::move_only_function<int (long) const>, boost::compat::move_only_function<int (long) const>, 0>' requested here
  844 |         move_only_function<int( long )> e3( std::move( e2 ) );

Do you want me to push up my WIP branch?

The more I think about it, the const_cast is probably harmless here anyway because we correctly apply the cv-ref qualifiers at the invoke site.

I'm not sure what we should do to fix these kinds of function pointer cast errors.

@pdimov
Copy link
Member

pdimov commented Jan 16, 2026

Ah, I see.

Fewer const_casts will be needed if you declare the functions to take storage const& (or storage const* if you prefer) and then const_cast inside, instead of at the call site.

@cmazakas cmazakas force-pushed the fix/sbo-invoke-copy branch 2 times, most recently from eed8de3 to d9faeab Compare January 17, 2026 15:43
#endif

#ifdef _MSC_VER
#pragma warning(disable: 4789) // false buffer overrun warning in test_mutable_lambda()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is the buffer overrun warning?

@pdimov
Copy link
Member

pdimov commented Jan 18, 2026

Can you please structure this as: commit 1 - test additions without the 4789 warning suppression; commit 2 - changes to the implementation; commit 3 - warning suppression in the test?

This prevents the code from erroneously copying the storage which causes
surprising behavior as noted here:
boostorg#23
The msvc optimizer runs into a bug in the mutable lambda tests for
move_only_function where it believes we're placing a large object into
the small buffer, resulting in compiler errors:

    D:\a\compat\boost-root\boost\compat\move_only_function.hpp(400) :
    error C2220: the following warning is treated as an error
    D:\a\compat\boost-root\boost\compat\move_only_function.hpp(400) :
    warning C4789: in function 'void __cdecl test_mutable_lambda(void)'
    buffer 'func2' of size 32 bytes will be overrun; 256 bytes will be
    written starting at offset 0

The code, however, is correct and eschews the SBO path for sufficiently
large Callables, as if one manually adds a `throw 1234;` statement to
the SBO paths, they are not hit when the test case in question is run.

We choose to manually suppress the warning as no other compiler emits it
nor is it found by any of our sanitizer CI jobs, including locally.
@cmazakas cmazakas force-pushed the fix/sbo-invoke-copy branch from d9faeab to 01d7279 Compare January 20, 2026 15:42
@pdimov
Copy link
Member

pdimov commented Jan 20, 2026

I've merged the first two commits to develop.

As for the third one, the code is correct, the branch is never taken. However, there's still a better fix because the reason for the warning is that manage_local<VT> is still instantiated for large VT, even though it's not called. That's because here

if( !storage::use_sbo<VT>() )

the compile-time constant condition storage::use_sbo<VT>() is tested with a runtime if.

if constexpr would of course have solved this, but since it's C++17, another approach would be to add another true_type/false_type to init, one that takes std::integral_constant<bool, storage::use_sbo()>().

A drive-by comment:

new(s.addr()) VT( std::move( *p ) );

Best practices when using placement new is to qualify it, ::new, so that the (unlikely) possibility of class-specific placement new is avoided.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

move_only_function does not carry mutable lambda state over multiple calls

2 participants