Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
47 changes: 47 additions & 0 deletions .github/workflows/gh-actions.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
name: Build & Test

on:
push:
branches: [ master, feat/psi-modernization ]
pull_request:
branches: [ master ]

jobs:
build:
name: ${{ matrix.config.name }} — ${{ matrix.build_type }}
runs-on: ${{ matrix.config.os }}
timeout-minutes: 20

strategy:
fail-fast: false
matrix:
config:
- { os: ubuntu-latest, name: Clang, cc: clang, cxx: clang++, gen: '-G Ninja' }
- { os: macos-26, name: Apple-Clang, cc: clang, cxx: clang++, gen: '-G Ninja' }
build_type: [Debug, Release]

steps:
- uses: actions/checkout@v4

- name: Install LLVM/Clang 22 (Linux)
if: matrix.config.name == 'Clang'
run: |
wget https://apt.llvm.org/llvm.sh
chmod u+x llvm.sh
sudo ./llvm.sh 22 all
echo "CC=/usr/lib/llvm-22/bin/clang" >> $GITHUB_ENV
echo "CXX=/usr/lib/llvm-22/bin/clang++" >> $GITHUB_ENV

- name: Configure
run: |
cmake -B build ${{ matrix.config.gen }} \
-DCMAKE_CXX_COMPILER="${CXX:-${{ matrix.config.cxx }}}" \
-DCMAKE_C_COMPILER="${CC:-${{ matrix.config.cc }}}" \
-DCMAKE_BUILD_TYPE=${{ matrix.build_type }} \
-S .

- name: Build & test
run: |
cmake --build build --config ${{ matrix.build_type }} -j
cd build/test
ctest --output-on-failure -C ${{ matrix.build_type }}
66 changes: 66 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
cmake_minimum_required( VERSION 3.27.0 )

project( Psi.Functionoid CXX )

###################
## Deps
###################

file(
DOWNLOAD
https://github.com/cpm-cmake/CPM.cmake/releases/latest/download/get_cpm.cmake
${CMAKE_CURRENT_BINARY_DIR}/cmake/get_cpm.cmake
)
set( CPM_SOURCE_CACHE "${CMAKE_CURRENT_BINARY_DIR}/deps" )

if ( APPLE )
set( GIT_EXECUTABLE /usr/bin/git CACHE FILEPATH "Path to git executable" FORCE )
endif()

include( ${CMAKE_CURRENT_BINARY_DIR}/cmake/get_cpm.cmake )

set( boost_ver boost-1.90.0 )
CPMAddPackage( "gh:boostorg/static_assert#${boost_ver}" )
CPMAddPackage( "gh:boostorg/throw_exception#${boost_ver}" )
CPMAddPackage( "gh:boostorg/config#${boost_ver}" )
CPMAddPackage( "gh:boostorg/type_traits#${boost_ver}" )
CPMAddPackage( "gh:boostorg/assert#${boost_ver}" )
CPMAddPackage( "gh:boostorg/core#${boost_ver}" )
CPMAddPackage( "gh:boostorg/move#${boost_ver}" )

CPMAddPackage( "gh:psiha/config_ex#master" )
CPMAddPackage( "gh:psiha/build#master" )

if ( NOT TARGET Boost::boost )
add_library( Boost::boost INTERFACE IMPORTED )
target_link_libraries( Boost::boost INTERFACE
Boost::config Boost::assert Boost::core Boost::move Boost::type_traits
)
target_include_directories( Boost::boost INTERFACE "${config_ex_SOURCE_DIR}/include" )
endif()

if ( ${CMAKE_SYSTEM_NAME} MATCHES "Linux" AND CMAKE_CXX_COMPILER_ID MATCHES "Clang" )
set( PSI_USE_LINKER "lld" )
endif()
include( ${build_SOURCE_DIR}/build_options.cmake )

set( CMAKE_CXX_STANDARD 26 )
add_compile_definitions( BOOST_ALL_NO_LIB )
if ( WIN32 )
add_compile_definitions( WIN32_LEAN_AND_MEAN NOMINMAX )
endif()

###################
## Target
###################

include( ${CMAKE_CURRENT_LIST_DIR}/functionoid.cmake )

###################
## Tests
###################

if ( PROJECT_IS_TOP_LEVEL )
enable_testing()
add_subdirectory( test )
endif()
200 changes: 114 additions & 86 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,86 +1,114 @@
#### Old notes (before conversion to C++14, renaming to functionoid/callable)

An alternate boost::function implementation that
- minimizes template and RTTI bloat
- has (much) less runtime overhead (for invocation, copy-construction and assignment)
- provides configurability through policies.

It is intended to be a drop-in replacement hence the same folder name as the
original.

For more information see the relevant discussion threads at boost.devel:
http://lists.boost.org/Archives/boost/2010/10/172593.php
http://thread.gmane.org/gmane.comp.lib.boost.devel/194514/focus=195351
http://article.gmane.org/gmane.comp.lib.boost.devel/196906.

Related undertakings:
- https://github.com/mamedev/delegates
- http://www.codeproject.com/Articles/7150/Member-Function-Pointers-and-the-Fastest-Possible
- http://www.codeproject.com/Articles/11015/The-Impossibly-Fast-C-Delegates
- http://www.codeproject.com/Articles/18389/Fast-C-Delegate-Boost-Function-drop-in-replacement
- http://www.codeproject.com/Articles/13287/Fast-C-Delegate
- http://stackoverflow.com/questions/4298408/5-years-later-is-there-something-better-than-the-fastest-possible-c-delegate
- http://www.gamedev.net/topic/549468-fast-c-delegates/
- http://codereview.stackexchange.com/questions/14730/impossibly-fast-delegate-in-c11
- https://probablydance.com/2013/01/13/a-faster-implementation-of-stdfunction
- move-only problem:
- http://lwg.github.io/issues/lwg-defects.html#1287
- http://stackoverflow.com/questions/25330716/move-only-version-of-stdfunction
- https://groups.google.com/forum/#!topic/comp.lang.c++.moderated/OFAMx695NJg
- https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/hFBaATcX0Wo

Currently this (alternate) code is based on original code from the
boost::function official 1.43 release (but it has been rebased on the current master
as part of the move from the sandbox to github). For the originial documentation see
http://www.boost.org/doc/libs/1_43_0/doc/html/function.html.

It was tested (compiling and running real world projects, building Boost and
running Boost regression tests) with the following compilers:
- MSVC++ 8.0 SP1, 10.0 (x86 and x64)
- GCC 4.0.1, 4.2.1, 4.5.1
- Clang 2.0, 2.8
- Sun Studio 5.9, 5.10
- VisualAge 11.1.


For the sake of brevity of future commit messages, comments etc. allow me to
add a few terminology definitions. Looking at the boost/tr1::function<> design/
idea we can identify two 'domains':
- the "front side" or the interface side or the "fixed type" or ABI side. The
interface that the user sees and the code that exists and executes _before_ the
call through a function pointer in the vtable (e.g. the code in operator()
before the invoke call is made, or code in operator= before calls to the manager
are made ...)
- the "back side" or that which stands behind the 'function pointer brick wall'/
'type erasure' (the actual invoker and the actual manager), which changes with
each assignement, which, at the point of change or creation has all the type
information it wants.

A few more notes:
- the default interface and behaviour remain the same (current code should work
the same without modification with these changes, "or so it seems to me")
- the empty handler objects are required to satisfy the is_stateless<>
condition (even though the current code should work even w/o this requirement
being satisfied)
- all function objects used with boost::function<> are expected to have a
nothrow destructor
- the swap and non-trivial assignment function do not offer the strong
exception safety guarantee in the special case when one or both of the objects
at stake is or has (if it is a boost::function<> object "holding" some function
object) a function object that fits in the function_buffer but does not have a
nothrow copy constructor and therefore does not have a no-fail move
operation...in this case it can happen that the final move operation (in the
swap procedure) from the tmp object to *this can fail and then the attempt to
restore (move) the original *this from a backup location can also fail leaving
us with no valid object in *this...in this situation the *this function object
is cleared (the EmptyHandler is assigned)...as far as i could tell the original
boost::function<> implementation had the same behaviour...
- a few more ideas of what can be made a matter of policy: size and alignment
of the function buffer (perhaps some library uses only complex targets so it
would benefit if the function_buffer optimization would be eliminated all
together...on the other some other library would benefit if the buffer would be
better aligned or enlarged by "just one int" and then no memory allocation
would take place for that library ...etc...)
- there is the ever present problem of what to return from empty handlers for
non-void and non-default constructible return types...
...
# Psi.Functionoid

Header-only, policy-driven type erasure for callables. The library descends from
[Boost.Functionoid](https://github.com/psiha/functionoid) (Domagoj Šarić’s
alternate `boost::function`); it is modernized for C++14+ and lives under the
`psi::functionoid` namespace with CMake target `Psi::Functionoid`.

Two public surfaces:

| Type | Role |
|------|------|
| **`callable`** | Owning wrapper — configurable `std::function` replacement |
| **`function_ref`** | Non-owning view — configurable `std::function_ref` replacement |

## Why not `std::function`?

`std::function` fixed the ergonomics of type-erased callbacks, but the design is
largely frozen: one size fits all (always copyable, fixed SBO, RTTI-based
`target_type`/`target`, empty handler throws). Hot paths pay for features they
never use.

**Psi.Functionoid `callable`** keeps the familiar “assign anything callable” model
but makes behaviour a **compile-time policy** (`default_traits` and custom
specializations):

- **SBO tuning** — buffer size and alignment are traits, not hard-coded.
- **Copy / move / destroy levels** — `support_level::{na,supported,nofail,trivial}`
so move-only, noexcept-move, or trivial-destruction paths need no copy vtable
slots (sweater `work_t` uses move-only + trivial dtor).
- **Empty handler** — throw, assert, or no-op on invoke of an empty wrapper.
- **RTTI** — on or off (`default_traits` disables it).
- **Noexcept signature** — trait-driven `noexcept` on `operator()`.
- **Vtable call-site hints** — optional `gnu::pure`, `clang::preserve_most`, … on
invoke/manager function pointers (`detail::vtable_attrs.hpp`).
- **Less bloat** — front/back split inherited from Functionoid: thin fixed ABI
“front”, typed invoker/manager “back”; fewer template instantiations and less
RTTI than classic `boost::function`.

Typical wins: lower per-call overhead, smaller hot-loop binaries, and wrappers
sized to the embedding project instead of the standard library’s compromise.

## Compared to the standard “fix attempts”

C++23 **`std::move_only_function`** and the ongoing **`std::copyable_function`**
proposal address the worst parts of `std::function` (move-only storage, clearer
empty behaviour) but remain **non-configurable**: you pick the standard type, not
policies. You cannot turn off RTTI, shrink the SBO for cache footprint, mark
vtable calls `[[gnu::pure]]`, or select assert-on-empty for debug builds.

**Psi.Functionoid** is the layer below those typedefs: one `callable` template,
many shapes — including move-only noexcept aliases that match or beat
`std::move_only_function` for scheduler/work-queue use, and copyable
`std_traits` for drop-in `std::function` semantics when you need them.

## `function_ref`

Non-owning type-erased reference to a callable with signature `R(Args...)`
(optionally `noexcept`). Same conceptual slot as **`std::function_ref`**
(P2637): pass a callback into an API without allocating or owning it.

Optimizations beyond a naive vtable indirection:

1. **Pointer-sized SBO** — if the decayed callable is trivially copyable and
fits in the storage word (`sizeof(void*)`), it is **embedded inline** and
invoked without an extra load.
2. **Borrowed object** — otherwise the ref holds a pointer to the caller’s
callable (lifetime is the caller’s responsibility, as with `function_ref`).

### Exception tunneling (C / V8 / other `noexcept` APIs)

Many foreign interfaces only accept **`noexcept` function pointers** — C
callbacks, V8 embedder hooks, etc. C++ callables that may throw cannot be passed
directly.

When the source callable is not `noexcept`-compatible, `function_ref` wraps it in
an **`exception_tunneling_callable`**: the exported callback is `noexcept`, catches
all exceptions into a `std::exception_ptr`, returns a default-constructed `R`
(or `void`), and the caller rethrows via **`check_failure()`** after the foreign
call returns. See `make_exception_tunneling_callable` and `make_c_callback` in
`include/psi/functionoid/function_ref.hpp`.

## Quick start (standalone)

```bash
cmake -B build -G Ninja -DCMAKE_BUILD_TYPE=Release -S .
cmake --build build
cd build/test && ctest --output-on-failure
```

## Embedding (sweater / host projects)

- `functionoid.cmake` → `Psi::Functionoid` INTERFACE target
- Include: `#include <psi/functionoid/functionoid.hpp>` or `function_ref.hpp`
- sweater `work_t` = `psi::functionoid::callable<void(), worker_traits>`

## Traits

`default_traits` is copyable + RTTI-off. Hot paths use move-only noexcept variants
(`copyable=na`, `moveable=nofail`, `destructor=trivial`). Optional vtable function
pointer attributes via `PSI_FUNCTIONOID_DETAIL_INVOKE_FN_ATTR` — see
`include/psi/functionoid/detail/vtable_attrs.hpp`.

---

### History

An alternate Boost.Function implementation (minimize template/RTTI bloat, lower
runtime overhead, policy configurability). Lineage: Boost.Function 1.43, rebased
through the Functionoid fork. For original Boost.Function docs see
https://www.boost.org/doc/libs/1_43_0/doc/html/function.html .

Related discussion and delegate implementations are listed in the archived notes
at the bottom of this file’s git history; the design glossary (front side / back
side, empty handler, SBO policy) still applies to `callable`.
20 changes: 20 additions & 0 deletions functionoid.cmake
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#############################################################################
# Psi.Functionoid — header-only callable library.
#
# Embed via include( functionoid.cmake ) from a host CMake project or standalone
# CMakeLists.txt.
#############################################################################

set( _functionoid_include "${CMAKE_CURRENT_LIST_DIR}/include" )

if ( NOT TARGET PsiFunctionoid )
add_library( PsiFunctionoid INTERFACE )
add_library( Psi::Functionoid ALIAS PsiFunctionoid )

target_include_directories( PsiFunctionoid INTERFACE "${_functionoid_include}" )

# Boost bits functionoid headers pull (host usually supplies Boost::boost).
if ( TARGET Boost::boost )
target_link_libraries( PsiFunctionoid INTERFACE Boost::boost )
endif()
endif()
40 changes: 0 additions & 40 deletions include/boost/function.hpp

This file was deleted.

Loading
Loading