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?

@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.

Currently, the code branches on whether to use SBO or not using a
runtime check, i.e. a raw `if(x)`. This can trick the msvc optimizer
into believing the SBO path can be taken, which in the case of
sufficiently large Callables triggers a buffer overrun warning as
emplacing into the small buffer would exceed its size.

By updating the code to instead dispatch at compile-time via tags, the
msvc optimizer is no longer confused and the warning disappears as the
code path is now truly unreachable.
@cmazakas cmazakas force-pushed the fix/sbo-invoke-copy branch from 01d7279 to 4596e89 Compare January 22, 2026 03:40
@pdimov pdimov merged commit 6050e53 into boostorg:develop Jan 22, 2026
58 checks passed
@cmazakas cmazakas deleted the fix/sbo-invoke-copy branch January 22, 2026 15:24
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