Skip to content

Commit

Permalink
Merge pull request #61 from bluescarni/pr/ref_sem_docs
Browse files Browse the repository at this point in the history
Docs & implementation refinements
  • Loading branch information
bluescarni committed May 19, 2024
2 parents 220078d + 3fd1d5e commit 51382ea
Show file tree
Hide file tree
Showing 23 changed files with 704 additions and 244 deletions.
16 changes: 15 additions & 1 deletion doc/config.rst
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,16 @@ Configuration options

.. cpp:enum-class:: wrap_semantics

Enumerator representing the semantics chosen for a :cpp:class:`wrap` class.

.. cpp:enumerator:: value

Value semantics.

.. cpp:enumerator:: reference

:ref:`Reference semantics <ref_semantics>`.

.. cpp:var:: template <typename T, typename IFace> requires iface_with_impl<IFace, T> inline constexpr std::size_t holder_size

Helper to compute the total amount of memory (in bytes) needed to statically store in a :cpp:class:`wrap`
Expand All @@ -114,4 +124,8 @@ Configuration options
- :cpp:var:`Cfg` is an instance of the primary :cpp:class:`config` template,
- :cpp:var:`config::static_align` is a power of two,
- :cpp:var:`config::explicit_ctor` is one of the enumerators defined in :cpp:enum:`wrap_ctor`,
- :cpp:var:`config::semantics` is one of the enumerators defined in :cpp:enum:`wrap_semantics`.
- :cpp:var:`config::semantics` is one of the enumerators defined in :cpp:enum:`wrap_semantics`,
- if :cpp:var:`config::copyable` is set to ``true``, so is :cpp:var:`config::movable` (that is,
a copyable :cpp:class:`wrap` must also be movable),
- if :cpp:var:`config::movable` is set to ``true``, so is :cpp:var:`config::swappable` (that is,
a movable :cpp:class:`wrap` must also be swappable).
57 changes: 34 additions & 23 deletions doc/custom_construct.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,6 @@ for which an empty state is meaningful.
The other option is to specify a custom (i.e., non-``void``) :cpp:type:`~config::DefaultValueType`
as first template argument in :cpp:struct:`config`, in which case the default constructor of
:cpp:class:`wrap` will value-initialise an interal value of type :cpp:type:`~config::DefaultValueType`.
Note that a custom :cpp:type:`~config::DefaultValueType` must satisfy the requirements
of the interface being wrapped by :cpp:class:`wrap`.

If both the :cpp:var:`config::invalid_default_ctor` option is activated and
a custom :cpp:type:`~config::DefaultValueType` is specified, then the :cpp:var:`config::invalid_default_ctor` option
Expand Down Expand Up @@ -61,6 +59,36 @@ Note that the invalid status is only about whether or not a :cpp:class:`wrap`
contains a value -- a valid wrap could be containing a moved-from value,
and it is up to the user to handle such an occurrence.

.. _wrap_construct:

Customising :cpp:class:`wrap`'s constructors
--------------------------------------------

Beside the :ref:`default constructor <def_ctor>`, it is also possible to customise
the behaviour of :cpp:class:`wrap`'s other constructors in a variety of ways.

:cpp:class:`wrap`'s generic constructors, for instance, are by default ``explicit``,
but they can be made implicit by altering the :cpp:var:`config::explicit_ctor`
configuration setting. If :cpp:var:`config::explicit_ctor` is set to
:cpp:enumerator:`wrap_ctor::ref_implicit`, then generic construction is implicit
when :ref:`wrapping a reference <wrap_reference>`, ``explicit`` otherwise.
If :cpp:var:`config::explicit_ctor` is set to
:cpp:enumerator:`wrap_ctor::always_implicit`, then generic construction is always implicit.

By default, :cpp:class:`wrap` is copy/move constructible and copy/move assignable.
The copy/move constructors and assignment operators can be disabled by switching off
the :cpp:var:`config::copyable` and :cpp:var:`config::movable` configuration settings.
The :cpp:class:`wrap` is also by default `swappable <https://en.cppreference.com/w/cpp/concepts/swappable>`__.
Swappability can be switched on/off via the :cpp:var:`config::swappable` configuration setting.

Copyability, movability and swappability are subject to several sanity checks and constraints.
Specifically:

- a :cpp:var:`~config::copyable` :cpp:class:`wrap` must also be :cpp:var:`~config::movable`,
and a :cpp:var:`~config::movable` :cpp:class:`wrap` must also be :cpp:var:`~config::swappable`;
- when employing value semantics (the default), it is not possible to create a copyable/movable/swappable
:cpp:class:`wrap` containing a value type which is not also copyable/movable/swappable.

.. _emplacement:

Emplacement
Expand Down Expand Up @@ -93,7 +121,10 @@ We can emplace-construct a :cpp:class:`wrap` containing a ``std::mutex``:

Note that in this specific case the variadic pack is empty (as the constructor of
``std::mutex`` takes no arguments) and thus only the ``std::in_place_type_t`` argument
is required.
is required. Note also that we used a custom configuration in which we disabled copy/move/swap
operations: this is necessary because ``std::mutex`` cannot be copied/moved/swapped, and thus,
as explained in the :ref:`previous section <wrap_construct>`, the :cpp:class:`wrap`'s copy/move/swap primitives must
also be disabled.

In addition to the emplace constructor, the :cpp:func:`~wrap::emplace()` function is also
available to emplace-assign a value into an existing :cpp:class:`wrap` object. Here
Expand All @@ -103,23 +134,3 @@ is a simple example:
:language: c++
:lines: 22-26

Customising :cpp:class:`wrap`'s constructors
--------------------------------------------

Beside the :ref:`default constructor <def_ctor>`, it is also possible to customise
the behaviour of :cpp:class:`wrap`'s other constructors in a variety of ways.

:cpp:class:`wrap`'s generic constructors, for instance, are by default ``explicit``,
but they can be made implicit by altering the :cpp:var:`config::explicit_ctor`
configuration setting. If :cpp:var:`config::explicit_ctor` is set to
:cpp:enumerator:`wrap_ctor::ref_implicit`, then generic construction is implicit
when :ref:`wrapping a reference <wrap_reference>`, ``explicit`` otherwise.
If :cpp:var:`config::explicit_ctor` is set to
:cpp:enumerator:`wrap_ctor::always_implicit`, then generic construction is always implicit.

By default, :cpp:class:`wrap` is copy/move constructible and copy/move assignable.
The copy/move constructors and assignment operators can be disabled by switching off
the :cpp:var:`config::copyable` and :cpp:var:`config::movable` configuration settings.

The :cpp:class:`wrap` is also by default `swappable <https://en.cppreference.com/w/cpp/types/is_swappable>`__.
Swappability can be switched on/off via the :cpp:var:`config::swappable` configuration setting.
9 changes: 5 additions & 4 deletions doc/hello_world.rst
Original file line number Diff line number Diff line change
Expand Up @@ -95,11 +95,12 @@ Although this code looks superficially similar to the OO-style approach, there a
key differences:

- no dynamic memory allocation is enforced: the :cpp:class:`wrap` class employs
an :ref:`optimisation <custom_storage>` that stores small values inline,
an :ref:`optimisation <custom_storage>` that stores small values inline;
- there is no hierarchical coupling: objects of any destructible class can be
stored in an ``any_wrap`` without the need to inherit from anything,
- ``any_wrap`` employs value semantics (that is, its copy constructor will make a copy
of the internal value).
stored in an ``any_wrap`` without the need to inherit from anything;
- ``any_wrap`` employs (by default) value semantics (that is, copy/move/swap operations
on a :cpp:class:`wrap` will result in copying/moving/swapping the internal
value).

And that's it for the most minimal example!

Expand Down
4 changes: 4 additions & 0 deletions doc/nonintrusive.rst
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,10 @@ a partial specialisation of the :cpp:struct:`iface_impl` struct for ``my_iface``
:language: c++
:lines: 17-29

That is, the :cpp:struct:`iface_impl` struct template depends on four parameters: the first one is the
the interface for which we are providing an implementation, while the remaining three are the
customary ``Base``, ``Holder`` and ``T`` arguments whose meaning has been explained in previous tutorials.

Here we are specifying an implementation for all value types ``T``, but, as explained
in previous tutorials, we could also easily provide a partially-specialised implementation,
constrain the implementation only for value types modelling
Expand Down
100 changes: 100 additions & 0 deletions doc/ref_semantics.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
.. _ref_semantics:

.. cpp:namespace-push:: tanuki

Reference semantics
===================

In the :ref:`first tutorial <hello_world>`, we mentioned how one of the reasons
to prefer type-erasure over traditional object-oriented programming is that with
type-erasure we can implement value semantics,
rather than being forced into pointer/reference semantics.

There are however situations in which pointer/reference semantics might be preferrable.
For instance, the motivating example for the development of reference semantics in tanuki
was the necessity to support large computational graphs featuring a high degree
of internal repetition in the `heyoka library <https://github.com/bluescarni/heyoka>`__.

Reference semantics support in tanuki is activated by setting the :cpp:var:`config::semantics`
config option to :cpp:enumerator:`wrap_semantics::reference`. When reference semantics
is activated, the :cpp:class:`wrap` class will employ internally a
`shared pointer <https://en.cppreference.com/w/cpp/memory/shared_ptr>`__ to store the type-erased
value, and copy/move/swap operations on a :cpp:class:`wrap` will behave like the corresponding
operations on a shared pointer - that is, they will manipulate references to the internal value,
rather than the value itself.

Additionally, there are a couple of extra functions in the API that are available only when
using reference semantics.

The first one, :cpp:func:`wrap::copy()`, is used to make a deep-copy of a :cpp:class:`wrap`
(that is, it enforces the creation of a copy of the internal value, rather than creating
a new shared reference to it).

The second one, :cpp:func:`wrap::same_value()`, can be used to detect if two :cpp:class:`wrap`
objects share ownership of the same value.

Let us see a simple example of reference semantics in action. We begin with our usual, super-simple
interface and its implementation:

.. literalinclude:: ../tutorial/ref_semantics.cpp
:language: c++
:lines: 7-14

We introduce a :cpp:class:`wrap` type employing reference semantics:

.. literalinclude:: ../tutorial/ref_semantics.cpp
:language: c++
:lines: 18-18

And we create a :cpp:class:`wrap` instance storing a ``std::string``:

.. literalinclude:: ../tutorial/ref_semantics.cpp
:language: c++
:lines: 20-20

Let us now make a copy of ``w1``:

.. literalinclude:: ../tutorial/ref_semantics.cpp
:language: c++
:lines: 22-22

As explained earlier, this operation will not copy the string inside ``w1``, rather it will create a new reference to it.
We can confirm that this indeed is the case by comparing the addresses of the values stored in ``w1`` and ``w2``:

.. literalinclude:: ../tutorial/ref_semantics.cpp
:language: c++
:lines: 26-27

.. code-block:: console
Address of the value wrapped by w1: 0x606000000038
Address of the value wrapped by w2: 0x606000000038
We can also invoke the :cpp:func:`wrap::same_value()` function for further confirmation:

.. literalinclude:: ../tutorial/ref_semantics.cpp
:language: c++
:lines: 29-29

.. code-block:: console
Do w1 and w2 share ownership? true
If, on the other hand, we use the :cpp:func:`wrap::copy()` function, we will be performing a copy
of the string stored in ``w1``:

.. literalinclude:: ../tutorial/ref_semantics.cpp
:language: c++
:lines: 31-36

.. code-block:: console
Address of the value wrapped by w1: 0x606000000038
Address of the value wrapped by w3: 0x606000000098
Do w1 and w3 share ownership? false
Full code listing
-----------------

.. literalinclude:: ../tutorial/ref_semantics.cpp
:language: c++
4 changes: 2 additions & 2 deletions doc/simple_interface.rst
Original file line number Diff line number Diff line change
Expand Up @@ -163,7 +163,7 @@ What happens now if we try to construct a ``foo3_wrap`` from an ``int``? The err
That is, the constructor of the :cpp:class:`wrap` class now detects that ``int`` does not have an
interface implementation, and as a consequence the compiler detects an error *before* trying to
invoke the (non-existing) ``foo()`` member function on the ``int``. We can confirm
that the non-constructability of ``foo3_wrap`` from ``int`` is detected at compile time
that the non-constructibility of ``foo3_wrap`` from ``int`` is detected at compile time
by checking the ``std::is_constructible`` type trait:

.. literalinclude:: ../tutorial/simple_interface.cpp
Expand Down Expand Up @@ -219,7 +219,7 @@ It works! Bu what happens if we try to construct a ``foo4_wrap`` from an object
``fooable`` nor an ``int``? The :cpp:class:`wrap` class will detect
that the interface implementation corresponding to an object of such type is empty (i.e., invalid),
and it will thus disable the constructor. We can confirm that this is the case by checking
the constructability of ``foo4_wrap`` from a ``float`` (which is neither
the constructibility of ``foo4_wrap`` from a ``float`` (which is neither
``fooable`` nor an ``int``):

.. literalinclude:: ../tutorial/simple_interface.cpp
Expand Down
13 changes: 5 additions & 8 deletions doc/std_function.rst
Original file line number Diff line number Diff line change
Expand Up @@ -3,16 +3,13 @@
Implementing a ``std::function`` look-alike
===========================================

In this case study, we will be implementing (an approximation of)
In this case study, we will be implementing an approximation of
``std::function`` with tanuki. This will not be a full drop-in
replacement for ``std::function``, because there is a (small) part of the ``std::function`` API which cannot
currently be implemented with tanuki, and because we aim to write an implementation which, contrary to ``std::function``
replacement for ``std::function``, because we aim to write an implementation which, contrary to ``std::function``
and similarly to `std::move_only_function <https://en.cppreference.com/w/cpp/utility/functional/move_only_function>`__,
correctly respects const-correctness.

As a bonus though, our polymorphic function wrapper (which we will be naming ``callable``) will also
support wrapping references (thus mimicking
`std::function_ref <https://en.cppreference.com/w/cpp/utility/functional/function_ref>`__ in some sense).
correctly respects const-correctness. Additionally, our polymorphic function wrapper (which we will be naming ``callable``)
will also support wrapping references (thus providing functionality similar to
`std::function_ref <https://en.cppreference.com/w/cpp/utility/functional/function_ref>`__).

The interface and its implementation
------------------------------------
Expand Down
1 change: 1 addition & 0 deletions doc/tutorials.rst
Original file line number Diff line number Diff line change
Expand Up @@ -12,3 +12,4 @@ Tutorials
custom_construct.rst
composite_interfaces.rst
nonintrusive.rst
ref_semantics.rst
Loading

0 comments on commit 51382ea

Please sign in to comment.