Giter Club home page Giter Club logo

dyno's Introduction

Dyno: Runtime polymorphism done right

Travis status

DISCLAIMER

At this point, this library is experimental and it is a pure curiosity. No stability of interface or quality of implementation is guaranteed. Use at your own risks.

Overview

Dyno solves the problem of runtime polymorphism better than vanilla C++ does. It provides a way to define interfaces that can be fulfilled non-intrusively, and it provides a fully customizable way of storing polymorphic objects and dispatching to virtual methods. It does not require inheritance, heap allocation or leaving the comfortable world of value semantics, and it can do so while outperforming vanilla C++.

Dyno is pure-library implementation of what's also known as Rust trait objects, Go interfaces, Haskell type classes, and virtual concepts. Under the hood, it uses a C++ technique known as type erasure, which is the idea behind std::any, std::function and many other useful types.

#include <dyno.hpp>
#include <iostream>
using namespace dyno::literals;

// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires_(
  "draw"_s = dyno::method<void (std::ostream&) const>
)) { };

// Define how concrete types can fulfill that interface
template <typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
  "draw"_s = [](T const& self, std::ostream& out) { self.draw(out); }
);

// Define an object that can hold anything that can be drawn.
struct drawable {
  template <typename T>
  drawable(T x) : poly_{x} { }

  void draw(std::ostream& out) const
  { poly_.virtual_("draw"_s)(out); }

private:
  dyno::poly<Drawable> poly_;
};

struct Square {
  void draw(std::ostream& out) const { out << "Square"; }
};

struct Circle {
  void draw(std::ostream& out) const { out << "Circle"; }
};

void f(drawable const& d) {
  d.draw(std::cout);
}

int main() {
  f(Square{}); // prints Square
  f(Circle{}); // prints Circle
}

Alternatively, if you find this to be too much boilerplate and you can stand using a macro, the following is equivalent:

#include <dyno.hpp>
#include <iostream>

// Define the interface of something that can be drawn
DYNO_INTERFACE(Drawable,
  (draw, void (std::ostream&) const)
);

struct Square {
  void draw(std::ostream& out) const { out << "Square"; }
};

struct Circle {
  void draw(std::ostream& out) const { out << "Circle"; }
};

void f(Drawable const& d) {
  d.draw(std::cout);
}

int main() {
  f(Square{}); // prints Square
  f(Circle{}); // prints Circle
}

Compiler requirements

This is a C++17 library. No efforts will be made to support older compilers (sorry). The library is known to work with the following compilers:

Compiler Version
GCC >= 7
Clang >= 4.0
Apple Clang >= 9.1

Dependencies

The library depends on Boost.Hana and Boost.CallableTraits. The unit tests depend on libawful and the benchmarks depend on Google Benchmark, Boost.TypeErasure and Mpark.Variant, but you don't need them to use the library. For local development, the dependencies/install.sh script can be used to install all the dependencies automatically.

Building the library

Dyno is a header-only library, so there's nothing to build per-se. Just add the include/ directory to your compiler's header search path (and make sure the dependencies are satisfied), and you're good to go. However, there are unit tests, examples and benchmarks that can be built:

(cd dependencies && ./install.sh) # Install dependencies; will print a path to add to CMAKE_PREFIX_PATH
mkdir build
(cd build && cmake .. -DCMAKE_PREFIX_PATH="${PWD}/../dependencies/install") # Setup the build directory

cmake --build build --target examples   # Build and run the examples
cmake --build build --target tests      # Build and run the unit tests
cmake --build build --target check      # Does both examples and tests
cmake --build build --target benchmarks # Build and run the benchmarks

Introduction

In programming, the need for manipulating objects with a common interface but with a different dynamic type arises very frequently. C++ solves this with inheritance:

struct Drawable {
  virtual void draw(std::ostream& out) const = 0;
};

struct Square : Drawable {
  virtual void draw(std::ostream& out) const override final { ... }
};

struct Circle : Drawable {
  virtual void draw(std::ostream& out) const override final { ... }
};

void f(Drawable const* drawable) {
  drawable->draw(std::cout);
}

However, this approach has several drawbacks. It is

  1. Intrusive
    In order for Square and Circle to fulfill the Drawable interface, they both need to inherit from the Drawable base class. This requires having the license to modify those classes, which makes inheritance very inextensible. For example, how would you make a std::vector<int> fulfill the Drawable interface? You simply can't.

  2. Incompatible with value semantics
    Inheritance requires you to pass polymorphic pointers or references to objects instead of the objects themselves, which plays very badly with the rest of the language and the standard library. For example, how would you copy a vector of Drawables? You'd need to provide a virtual clone() method, but now you've just messed up your interface.

  3. Tightly coupled with dynamic storage
    Because of the lack of value semantics, we usually end up allocating these polymorphic objects on the heap. This is both horribly inefficient and semantically wrong, since chances are we did not need the dynamic storage duration at all, and an object with automatic storage duration (e.g. on the stack) would have been enough.

  4. Prevents inlining
    95% of the time, we end up calling a virtual method through a polymorphic pointer or reference. That requires three indirections: one for loading the pointer to the vtable inside the object, one for loading the right entry in the vtable, and one for the indirect call to the function pointer. All this jumping around makes it difficult for the compiler to make good inlining decisions. However, it turns out that all of these indirections except the indirect call can be avoided.

Unfortunately, this is the choice that C++ has made for us, and these are the rules that we are bound to when we need dynamic polymorphism. Or is it really?

So, what is this library?

Dyno solves the problem of runtime polymorphism in C++ without any of the drawbacks listed above, and many more goodies. It is:

  1. Non-intrusive
    An interface can be fulfilled by a type without requiring any modification to that type. Heck, a type can even fulfill the same interface in different ways! With Dyno, you can kiss ridiculous class hierarchies goodbye.

  2. 100% based on value semantics
    Polymorphic objects can be passed as-is, with their natural value semantics. You need to copy your polymorphic objects? Sure, just make sure they have a copy constructor. You want to make sure they don't get copied? Sure, mark it as deleted. With Dyno, silly clone() methods and the proliferation of pointers in APIs are things of the past.

  3. Not coupled with any specific storage strategy
    The way a polymorphic object is stored is really an implementation detail, and it should not interfere with the way you use that object. Dyno gives you complete control over the way your objects are stored. You have a lot of small polymorphic objects? Sure, let's store them in a local buffer and avoid any allocation. Or maybe it makes sense for you to store things on the heap? Sure, go ahead.

  4. Flexible dispatch mechanism to achieve best possible performance
    Storing a pointer to a vtable is just one of many different implementation strategies for performing dynamic dispatch. Dyno gives you complete control over how dynamic dispatch happens, and can in fact beat vtables in some cases. If you have a function that's called in a hot loop, you can for example store it directly in the object and skip the vtable indirection. You can also use application-specific knowledge the compiler could never have to optimize some dynamic calls — library-level devirtualization.

Using the library

First, you start by defining a generic interface and giving it a name. Dyno provides a simple domain specific language to do that. For example, let's define an interface Drawable that describes types that can be drawn:

#include <dyno.hpp>
using namespace dyno::literals;

struct Drawable : decltype(dyno::requires_(
  "draw"_s = dyno::method<void (std::ostream&) const>
)) { };

This defines Drawable as representing an interface for anything that has a method called draw taking a reference to a std::ostream. Dyno calls these interfaces dynamic concepts, since they describe sets of requirements to be fulfilled by a type (like C++ concepts). However, unlike C++ concepts, these dynamic concepts are used to generate runtime interfaces, hence the name dynamic. The above definition is basically equivalent to the following:

struct Drawable {
  virtual void draw(std::ostream&) const = 0;
};

Once the interface is defined, the next step is to actually create a type that satisfies this interface. With inheritance, you would write something like this:

struct Square : Drawable {
  virtual void draw(std::ostream& out) const override final {
    out << "square" << std::endl;
  }
};

With Dyno, the polymorphism is non-intrusive and it is instead provided via what is called a concept map (after C++0x Concept Maps):

struct Square { /* ... */ };

template <>
auto const dyno::concept_map<Drawable, Square> = dyno::make_concept_map(
  "draw"_s = [](Square const& square, std::ostream& out) {
    out << "square" << std::endl;
  }
);

This construct is the specialization of a C++14 variable template named concept_map defined in the dyno:: namespace. We then initialize that specialization with dyno::make_concept_map(...).

The first parameter of the lambda is the implicit *this parameter that is implied when we declared draw as a method above. It's also possible to erase non-member functions (see the relevant section).

This concept map defines how the type Square satisfies the Drawable concept. In a sense, it maps the type Square to its implementation of the concept, which motivates the appellation. When a type satisfies the requirements of a concept, we say that the type models (or is a model of) that concept. Now that Square is a model of the Drawable concept, we'd like to use a Square polymorphically as a Drawable. With traditional inheritance, we would use a pointer to a base class like this:

void f(Drawable const* d) {
  d->draw(std::cout);
}

f(new Square{});

With Dyno, polymorphism and value semantics are compatible, and the way polymorphic types are passed around can be highly customized. To do this, we'll need to define a type that can hold anything that's Drawable. It is that type, instead of a Drawable*, that we'll be passing around to and from polymorphic functions. To help define this wrapper, Dyno provides the dyno::poly container, which can hold an arbitrary object satisfying a given concept. As you will see, dyno::poly has a dual role: it stores the polymorphic object and takes care of the dynamic dispatching of methods. All you need to do is write a thin wrapper over dyno::poly to give it exactly the desired interface:

struct drawable {
  template <typename T>
  drawable(T x) : poly_{x} { }

  void draw(std::ostream& out) const
  { poly_.virtual_("draw"_s)(out); }

private:
  dyno::poly<Drawable> poly_;
};

Note: You could technically use dyno::poly directly in your interfaces. However, it is much more convenient to use a wrapper with real methods than dyno::poly, and so writing a wrapper is recommended.

Let's break this down. First, we define a member poly_ that is a polymorphic container for anything that models the Drawable concept:

dyno::poly<Drawable> poly_;

Then, we define a constructor that allows constructing this container from an arbitrary type T:

template <typename T>
drawable(T x) : poly_{x} { }

The unsaid assumption here is that T actually models the Drawable concept. Indeed, when you create a dyno::poly from an object of type T, Dyno will go and look at the concept map defined for Drawable and T, if any. If there's no such concept map, the library will report that we're trying to create a dyno::poly from a type that does not support it, and your program won't compile.

Finally, the strangest and most important part of the definition above is that of the draw method:

void draw(std::ostream& out) const
{ poly_.virtual_("draw"_s)(out); }

What happens here is that when .draw is called on our drawable object, we'll actually perform a dynamic dispatch to the implementation of the "draw" function for the object currently stored in the dyno::poly, and call that. Now, to create a function that accepts anything that's Drawable, no need to worry about pointers and ownership in your interface anymore:

void f(drawable d) {
  d.draw(std::cout);
}

f(Square{});

By the way, if you're thinking that this is all stupid and you should have been using a template, you're right. However, consider the following, where you really do need runtime polymorphism:

drawable get_drawable() {
  if (some_user_input())
    return Square{};
  else
    return Circle{};
}

f(get_drawable());

Strictly speaking, you don't need to wrap dyno::poly, but doing so puts a nice barrier between Dyno and the rest of your code, which never has to worry about how your polymorphic layer is implemented. Also, we largely ignored how dyno::poly was implemented in the above definition. However, dyno::poly is a very powerful policy-based container for polymorphic objects that can be customized to one's needs for performance. Creating a drawable wrapper makes it easy to tweak the implementation strategy used by dyno::poly for performance without impacting the rest of your code.

Customizing the polymorphic storage

The first aspect that can be customized in a dyno::poly is the way the object is stored inside the container. By default, we simply store a pointer to the actual object, like one would do with inheritance-based polymorphism. However, this is often not the most efficient implementation, and that's why dyno::poly allows customizing it. To do so, simply pass a storage policy to dyno::poly. For example, let's define our drawable wrapper so that it tries to store objects up to 16 bytes in a local buffer, but then falls back to the heap if the object is larger:

struct drawable {
  template <typename T>
  drawable(T x) : poly_{x} { }

  void draw(std::ostream& out) const
  { poly_.virtual_("draw"_s)(out); }

private:
  dyno::poly<Drawable, dyno::sbo_storage<16>> poly_;
  //                   ^^^^^^^^^^^^^^^^^^^^^ storage policy
};

Notice that nothing except the policy changed in our definition. That is one very important tenet of Dyno; these policies are implementation details, and they should not change the way you write your code. With the above definition, you can now create drawables just like you did before, and no allocation will happen when the object you're creating the drawable from fits in 16 bytes. When it does not fit, however, dyno::poly will allocate a large enough buffer on the heap.

Let's say you actually never want to do an allocation. No problem, just change the policy to dyno::local_storage<16>. If you try to construct a drawable from an object that's too large to fit in the local storage, your program won't compile. Not only are we saving an allocation, but we're also saving a pointer indirection every time we access the polymorphic object if we compare to the traditional inheritance-based approach. By tweaking these (important) implementation details for you specific use case, you can make your program much more efficient than with classic inheritance.

Other storage policies are also provided, like dyno::remote_storage and dyno::non_owning_storage. dyno::remote_storage is the default one, which always stores a pointer to a heap-allocated object. dyno::non_owning_storage stores a pointer to an object that already exists, without worrying about the lifetime of that object. It allows implementing non-owning polymorphic views over objects, which is very useful.

Custom storage policies can also be created quite easily. See <dyno/storage.hpp> for details.

Customizing the dynamic dispatch

When we introduced dyno::poly, we mentioned that it had two roles; the first is to store the polymorphic object, and the second one is to perform dynamic dispatch. Just like the storage can be customized, the way dynamic dispatching is performed can also be customized using policies. For example, let's define our drawable wrapper so that instead of storing a pointer to the vtable, it instead stores the vtable in the drawable object itself. This way, we'll avoid one indirection each time we access a virtual function:

struct drawable {
  template <typename T>
  drawable(T x) : poly_{x} { }

  void draw(std::ostream& out) const
  { poly_.virtual_("draw"_s)(out); }

private:
  using Storage = dyno::sbo_storage<16>;                      // storage policy
  using VTable = dyno::vtable<dyno::local<dyno::everything>>; // vtable policy
  dyno::poly<Drawable, Storage, VTable> poly_;
};

Notice that nothing besides the vtable policy needs to change in the definition of our drawable type. Furthermore, if we wanted, we could change the storage policy independently from the vtable policy. With the above, even though we are saving all indirections, we are paying for it by making our drawable object larger (since it needs to hold the vtable locally). This could be prohibitive if we had many functions in the vtable. Instead, it would make more sense to store most of the vtable remotely, but only inline those few functions that we call heavily. Dyno makes it very easy to do so by using Selectors, which can be used to customize what functions a policy applies to:

struct drawable {
  template <typename T>
  drawable(T x) : poly_{x} { }

  void draw(std::ostream& out) const
  { poly_.virtual_("draw"_s)(out); }

private:
  using Storage = dyno::sbo_storage<16>;
  using VTable = dyno::vtable<
    dyno::local<dyno::only<decltype("draw"_s)>>,
    dyno::remote<dyno::everything_else>
  >;
  dyno::poly<Drawable, Storage, VTable> poly_;
};

Given this definition, the vtable is actually split in two. The first part is local to the drawable object and contains only the draw method. The second part is a pointer to a vtable in static storage that holds the remaining methods (the destructor, for example).

Dyno provides two vtable policies, dyno::local<> and dyno::remote<>. Both of these policies must be customized using a Selector. The selectors supported by the library are dyno::only<functions...>, dyno::except<...>, and dyno::everything_else (which can also be spelled dyno::everything).

Defaulted concept maps

When defining a concept, it is often the case that one can provide a default definition for at least some functions associated to the concept. For example, by default, it would probably make sense to use a member function named draw (if any) to implement the abstract "draw" method of the Drawable concept. For this, one can use dyno::default_concept_map:

template <typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
  "draw"_s = [](auto const& self, std::ostream& out) { self.draw(out); }
);

Now, whenever we try to look at how some type T fulfills the Drawable concept, we'll fall back to the default concept map if no concept map was defined. For example, we can create a new type Circle:

struct Circle {
  void draw(std::ostream& out) const {
    out << "circle" << std::endl;
  }
};

f(Circle{}); // prints "circle"

Circle is automatically a model of Drawable, even though we did not explicitly define a concept map for Circle. On the other hand, if we were to define such a concept map, it would have precedence over the default one:

template <>
auto dyno::concept_map<Drawable, Circle> = dyno::make_concept_map(
  "draw"_s = [](Circle const& circle, std::ostream& out) {
    out << "triangle" << std::endl;
  }
);

f(Circle{}); // prints "triangle"

Parametric concept maps

It is sometimes useful to define a concept map for a complete family of types all at once. For example, we might want to make std::vector<T> a model of Drawable, but only when T can be printed to a stream. This is easily achieved by using this (not so) secret trick:

template <typename T>
auto const dyno::concept_map<Drawable, std::vector<T>, std::void_t<decltype(
  std::cout << std::declval<T>()
)>> = dyno::make_concept_map(
  "draw"_s = [](std::vector<T> const& v, std::ostream& out) {
    for (auto const& x : v)
      out << x << ' ';
  }
);

f(std::vector<int>{1, 2, 3}) // prints "1 2 3 "

Notice how we do not have to modify std::vector at all. How could we do this with classic polymorphism? Answer: no can do.

Erasing non-member functions

Dyno allows erasing non-member functions and functions that are dispatched on an arbitrary argument (but only one argument) too. To do this, simply define the concept using dyno::function instead of dyno::method, and use the dyno::T placeholder to denote the argument being erased:

// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires_(
  "draw"_s = dyno::function<void (dyno::T const&, std::ostream&)>
)) { };

The dyno::T const& parameter used above represents the type of the object on which the function is being called. However, it does not have to be the first parameter:

struct Drawable : decltype(dyno::requires_(
  "draw"_s = dyno::function<void (std::ostream&, dyno::T const&)>
)) { };

The fulfillment of the concept does not change whether the concept uses a method or a function, but make sure that the parameters of your function implementation match that of the function declared in the concept:

// Define how concrete types can fulfill that interface
template <typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
  "draw"_s = [](std::ostream& out, T const& self) { self.draw(out); }
  //            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ matches the concept definition
);

Finally, when calling a function on a dyno::poly, you'll have to pass in all the parameters explicitly, since Dyno can't guess which one you want to dispatch on. The parameter that was declared with a dyno::T placeholder in the concept should be passed the dyno::poly itself:

// Define an object that can hold anything that can be drawn.
struct drawable {
  template <typename T>
  drawable(T x) : poly_{x} { }

  void draw(std::ostream& out) const
  { poly_.virtual_("draw"_s)(out, poly_); }
  //                              ^^^^^ passing the poly explicitly

private:
  dyno::poly<Drawable> poly_;
};

dyno's People

Contributors

badair avatar laurence6 avatar ldionne avatar ricejasonf avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

dyno's Issues

Consider adding assignment from anything

Basically, template <typename T> poly::operator=(T&& t). However, this suffers from possible problems when constructing with a possibly-throwing move constructor, which could cause poly to be in some kind of empty state. We have to think about this, because I'd rather not introduce an implicit "empty" state to poly.

Check signatures in concept maps

In a concept map, perform checks to ensure that functions have a signature compatible with that of the function they're supposed to fulfill.

Interface for using `te::concept_map` for static dispatch

Have you considered providing an interface that'd allow using the concept maps of this library for static dispatch, basically to provide a compile-time mechanism for customization points? Shouldn't be too hard to implement, judging by looking through the implementation.

I'm mainly interested in this because I've been working on a customization points thingy myself, although with slightly less Hana and slightly more Boost.PP (which is currently mostly necessary for my vision of the interface due to how we don't have reflection yet), and I'd love to be able to compare bananas to bananas.

The main difference between what I did (which I can hopefully get online soon) and what you did that I've been focused more on providing a customization point mechanism that sucks somewhat less than what's been available to date, and you've focused on providing runtime behavior; this is in essence very similar and requires a lot of common features.

Just few comments/questions not exactly related to the issue, I hope you don't mind putting them here:

  1. Are you going to present a talk about this at C++Now? (I've proposed a talk that's supposed to talk about my approach, so hopefully we can get some discussion about both the approaches if we both end up giving talks at the conference.)
  2. I believe (but I'm biased, so what do I know? :D) my end user interface is nicer than yours, unless until you hit templates, where the macro'd approach starts to get not very customizable at one point. More on point though, did you consider how te might look like when reflection becomes available? Mine should be mostly 1:1 identical, I wonder if yours is as easily translatable at the end user interface level.
  3. Have you thought about solving the problem of having to exit all of your namespaces to specialize the variable template in namespace te from some levels of end-user namespaces?

Consider supporting method definitions in concepts

This is really just bells and whistles, but it would be possible to have something like this:

struct Drawable : decltype(te::requires(
  "draw"_s = te::method<void (std::ostream&) const>
)) { };

instead of

struct Drawable : decltype(te::requires(
  "draw"_s = te::function<void (te::T const&, std::ostream&)>
)) { };

Of course, the first is just syntactic sugar for the second, but we could also have some more integration of methods with the rest of the library, so that when you write e.g.

poly.virtual_("draw"_s)(std::cout);

it actually passes the this pointer correctly if "draw"_s was defined as a method. This may or may not be worth the added complexity in both the implementation of the library and for learning the library. I really don't want to obscure what the library does with too many features, since it is dead simple at its heart.

Compile time code generation

Hi @ldionne

I was watching today your talk from CppCon 2017, and wanted to look how complex would it be to do what you do in C++, but instead use D programming language:

https://github.com/baryluk/poly.d

shows how simple it.

This is a proof of concept that I just wrote today quickly, but it does work. Adding extra features (inlined storage, sbo, etc), is rather simple, and not done, because it is actually pretty trivial.

Example of magic:

  final interface IVehicle {
    void accelerate(float x);
  }

  // MAGIC
  alias Vehicle = generate_poly!IVehicle;

  struct Truck {
    void accelerate(float x) { }
  }

  struct Car {
    void accelerate(float x) { }
  }

  // Some example function that receives any Vehicle type.
  void f(Vehicle v) {
    v.accelerate(2.0);
  }

  Truck truck1 = {5.0};
  Car car1;

  Vehicle[] vehicles = [Vehicle(truck1), Vehicle(rock1)];

  foreach (ref vehicle; vehicles) {
    f(vehicle);
  }

just an example.

Maybe C++ should use more ideas from D programming language. :)

Whole implementation is ~80 lines of code and fits on the single screen.

I am writing to you about this, because in your talk you said that the code would be horrible and leak. But it shows that it makes code significantly easier, shorter, easier to use and very well encapsulated.

Invalid expression in default concept map

The following does not compile:

// Copyright Louis Dionne 2017
// Distributed under the Boost Software License, Version 1.0.
// (See accompanying file LICENSE.md or copy at http://boost.org/LICENSE_1_0.txt)

#include "testing.hpp"

#include <te/concept.hpp>
#include <te/concept_map.hpp>
using namespace te::literals;


struct Concept : decltype(te::requires(
  "f"_s = te::function<int (te::T&)>,
  "g"_s = te::function<int (te::T&)>
)) { };

// Since the definition of `f` would be invalid, we need to use a generic
// lambda to delay the instantiation of the body. This test makes sure that
// this "workaround" works.
template <typename T>
static auto const te::default_concept_map<Concept, T> = te::make_default_concept_map<Concept, T>(
  "f"_s = [](auto& t) { t.invalid(); return 222; },
  "g"_s = [](auto& t) { t.valid(); return 333; }
);

struct Foo {
  void invalid() = delete;
  void valid() { }
};

template <>
auto const te::concept_map<Concept, Foo> = te::make_concept_map<Concept, Foo>(
  "f"_s = [](Foo&) { return 444; }
);

int main() {
  Foo foo;
  TE_CHECK(te::concept_map<Concept, Foo>["f"_s](foo) == 444);
  TE_CHECK(te::concept_map<Concept, Foo>["g"_s](foo) == 333);
}

Very nice introduction!

:-) Very nice readme! Carry on the good work :).

I am very curious to see more interesting documentation in the readme :-). One really has to read the introduction two times :-) to wrap it into ones head :-)!, nevertheless it is well written and makes superb analogies to the standard (unflexible ...) way of doing it.

Problem with GCC 8: Example does not compile b/c `poly::unerase_poly` is not declared.

Hi. I'm trying to compile the "long-form" example from the readme (i.e., the example without using the macro). However, it fails because apparently, poly::unerase_poly is not declared (at least not in a way that GCC expects it):

lukas@i11pcbarth ~/tmp/mnwe/dyno $ g++ --std=c++17 -I /home/lukas/src/dyno/include/ -I /home/lukas/opt/boost_install/include/ ./example.hpp 
In file included from /home/lukas/src/dyno/include/dyno/macro.hpp:33,
                 from /home/lukas/src/dyno/include/dyno.hpp:11,
                 from ./example.hpp:1:
/home/lukas/src/dyno/include/dyno/poly.hpp: In instantiation of ‘dyno::poly<Concept, Storage, VTablePolicy>::virtual_impl(dyno::method_t<R(T ...) const>, Function) const [with R = void; T = {std::basic_ostream<char, std::char_traits<char> >&}; Function = dyno::detail::string<'d', 'r', 'a', 'w'>; Concept = Drawable; Storage = dyno::remote_storage; VTablePolicy = dyno::vtable<dyno::remote<dyno::everything> >]::<lambda(auto:17&& ...)> [with auto:17 = {std::basic_ostream<char, std::char_traits<char> >&}]’:
./example.hpp:22:33:   required from here
/home/lukas/src/dyno/include/dyno/poly.hpp:221:19: error: ‘unerase_poly’ was not declared in this scope
                   poly::unerase_poly<T>(static_cast<decltype(args)&&>(args))...);
                   ^~~~

The directory /home/lukas/src/dyno/ contains a current git HEAD checkout, and /home/lukas/opt/boost_install contains a boost 1.66 (including Boost.Hana). I'm using GCC 8 on Ubuntu:

lukas@i11pcbarth ~/tmp/mnwe/dyno $ g++ -v
Using built-in specs.
COLLECT_GCC=/usr/bin/g++
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/8/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 8.1.0-5ubuntu1~16.04' --with-bugurl=file:///usr/share/doc/gcc-8/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-8 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 8.1.0 (Ubuntu 8.1.0-5ubuntu1~16.04) 

For completeness sake, here is the complete content of the example.hpp that I'm trying to compile:

#include <dyno.hpp>
#include <iostream>
using namespace dyno::literals;

// Define the interface of something that can be drawn
struct Drawable : decltype(dyno::requires(
  "draw"_s = dyno::method<void (std::ostream&) const>
)) { };

// Define how concrete types can fulfill that interface
template <typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
  "draw"_s = [](T const& self, std::ostream& out) { self.draw(out); }
);

// Define an object that can hold anything that can be drawn.
struct drawable {
  template <typename T>
  drawable(T x) : poly_{x} { }

  void draw(std::ostream& out) const
  { poly_.virtual_("draw"_s)(out); }

private:
  dyno::poly<Drawable> poly_;
};

struct Square {
  void draw(std::ostream& out) const { out << "Square"; }
};

struct Circle {
  void draw(std::ostream& out) const { out << "Circle"; }
};

void f(drawable const& d) {
  d.draw(std::cout);
}

int main() {
  f(Square{}); // prints Square
  f(Circle{}); // prints Circle
}

Consider support for concept hierarchies

Consider adding something like register_concept_hierarchy, which would allow looking into derived concepts when trying to fulfill the concept map of a base concept. To solve the same problem, we could also use something like global_concept_map, where a user would define a bunch of functions and the concept maps would pick what they need.

The "stack storage" that often isn't.

The readme sometimes refers to "stack" storage when what's meant is direct storage in an object member. Whether that object, and thus the member, is stored on stack, on the heap or in static storage is completely up to its user. The term "stack" should only be used when we're sure the object is actually on the stack.

Documentation revisions: additional reasons for using this library

Documentation revisions: additional reasons for using this library ie. why it, rust traits and go interfaces should be used 85++% of the time

  1. Ease of use and teaching from not overdesigning
    in many OOP implementations types start virtualized at least with a virtual desctructor as one does not know who will be inheriting from the class in the future. It is taught by default you should be putting virtual ~ClassName() on all your classes for this very reason. This is contrary to the C++ philosophy that you only pay for such if you are actually using it. This popularized code pattern has turned into an antipattern. It is tragic when a type is only accessible from within a module as the developer is in full control. It could still be over used on module public api, ie. between module, if destruction was never needed ie. object life time was not a factor of the algorithm's that operated on said type.

  2. Ease of use and teaching from not overdesigning
    C++ multiple inheritence. "there are no polymorphic types but rather polymorphic use"
    similar to the over design for the the unknown future in virtual destructors one must virtually inherit from other classes in case the class gets inherited from latter. The virtual keyword is again overused and the decision is made at class definition time instead of at class usage time.

  3. Modules which are great can encourage over design. Specifically that in 1) and 2). A lot of this, but not all, becomes unnecessary when using traits/dyno where the library writer only requires that which is actually being used.

...

This educates users on when they should be used and makes the case for its adoption into boost and future C++ standards.

Arity and multiple SBO

I don't know if any of this has merit. You know the whole storage versus execution trade off.
Is there compile time metadata on the arity or number of methods in the vtable?
If users were given a way to tag types as immutable then the following template constuctor pattern may be worth exploring. If the immutable object type is smaller or equal in size of its pointer then store a copy of the object instead. Also it may be possible to store a object of size 2 * pointer size - 1 byte if the number of virtual methods are less than 256 which will practically always be the case. In any case, the 2 pointer storage type should always be the same type. This logic, or at least part of it, being performed in the concept map instead of the poly.

Allow customizing policies independently

Right now, the storage policy comes first, which means that I have to specify a storage policy even if I only want to customize the vtable policy. It should be possible to provide the vtable policy first as well (basically, unordered template parameters).

Add examples of passing custom concept maps to poly

poly has a constructor that allows passing a custom concept map. This can be used like

if (condition) {
  dyno::poly<Concept> poly{x, concept_map_1};
} else {
  dyno::poly<Concept> poly{x, concept_map_2};
}

We should find a nice use case and document this.

'concept' and 'requires' are keywords in c++2a

This makes the library unusable with, for example, clang++ -std=c++2a.

I have a local patch that appends underscores to these names, but I'm not sure how much to change in the documentation comments.

Please create a C++20 proposal

Please create a C++20 proposal. I consider this as fundamental to developing as function_ref proposal and non member pointers. ... Possibly a simpler version just 2 pointers, no object storage or vtable storage customization. ... Really don't want to have to wait til 2023 when we get static reflection with injection to be able to make a library only version when this really could come sooner with language support.

Cannot find HanaConfig.cmake

Hello,
I tried to use your library with my project but I have this error with CMake :

  CMake Error at CMakeLists.txt:17 (find_package):
  By not providing "FindHana.cmake" in CMAKE_MODULE_PATH this project has
  asked CMake to find a package configuration file provided by "Hana", but
  CMake did not find one.

  Could not find a package configuration file provided by "Hana" with any of
  the following names:

    HanaConfig.cmake
    hana-config.cmake

I installed Boost 1.65.1 with Homebrew on macOS High Sierra, but it doesn't seem to have this file configured while I can add other Boost librairies in my CMake file.

Please submit to BOOST

Please submit to BOOST. This library is as fundamental to programming as virtual, polymorphism, function_ref and on member pointers.

"copy-construct" Function Not Found

I'm a little stumped here or maybe I found a bug. I have a couple of copy constructible classes plus a copy constructible "holder", but I am getting the following error:

/usr/local/src/emscripten/system/include/dyno/vtable.hpp:196:5: error: static_assert failed "dyno::joined_vtable::operator[]: Request for a virtual function that is not present in any of the joined vtables. Make sure you meant to look this function up, and otherwise check whether the two sub-vtables look as expected. You can find the contents of the joined vtables and the function you were trying to access in the compiler error message, probably in the following format: `joined_vtable<VTABLE 1, VTABLE 2>::get_function<FUNCTION NAME>`"
    static_assert(always_false,
    ^             ~~~~~~~~~~~~
/usr/local/src/emscripten/system/include/dyno/vtable.hpp:177:12: note: in instantiation of function template specialization 'dyno::joined_vtable<dyno::local_vtable<>, dyno::remote_vtable<dyno::local_vtable<boost::hana::pair<boost::hana::string<'c', 'o', 'p', 'y'>, boost::hana::basic_type<void (const dyno::T &, std::__2::__wrap_iter<char *>)> >, boost::hana::pair<boost::hana::string<'e', 'q', 'u', 'a', 'l'>, boost::hana::basic_type<bool (const dyno::T &, std::__2::__wrap_iter<const char *>)> >, boost::hana::pair<boost::hana::string<'l', 'e', 'n', 'g', 't', 'h'>, boost::hana::basic_type<unsigned int (const dyno::T &)> >, boost::hana::pair<dyno::detail::string<'d', 'e', 's', 't', 'r', 'u', 'c', 't'>, boost::hana::basic_type<void (dyno::T &)> >, boost::hana::pair<dyno::detail::string<'s', 't', 'o', 'r', 'a', 'g', 'e', '_', 'i', 'n', 'f', 'o'>, boost::hana::basic_type<dyno::storage_info ()> > > > >::get_function<dyno::detail::string<'c', 'o', 'p', 'y', '-', 'c', 'o', 'n', 's', 't', 'r', 'u', 'c', 't'> >' requested here
    return get_function(name, first_.contains(name), second_.contains(name));

I apologize for not embedding a minimal use case but here is the relevant code:

https://github.com/ricejasonf/nbdl/blob/edd82261cdfc0fd253c970478aab4271f33afebd/include/nbdl/detail/string_concat_view.hpp#L91

What's weird is that this is happening where I would expect RVO to prevent copying. Maybe I am just doing something really dumb.

Any help on this is greatly appreciated. 😄

Wrong static_assert in detail::erase

detail::erase<...>::apply(Arg&&) has a static_assert that should state

std::is_rvalue_reference<decltype(std::forward<Arg>(arg))>::value

instead of

std::is_rvalue_reference<Arg>::value

This is line 103 of eraser_traits.hpp.

DYNO_INTERFACE() customizable storage policy.

Hi Louis. Great work!
What is the default DYNO_INTERFACE() macro storage policy? You could consider a way to let the user specify the storage policy when using a new one / this macro.

Memory is not laundered after placement newing

It appears that you are placement newing objects with their constructors which you have stored in a v-table via some tricks........that is fine, except that it appears that you are just referencing the memory that is created via the same pointer that you created it with......technically this is UB. Since you are using c++17 you can use std::launder..........

Use a less generic user-defined literal

Due to issues with using user-defined literals without exposing the whole namespace and interfaces generally being defined in a header, it's pretty much required to release dyno::literals into your own namespace. The problem here is that _s is far too generic, could the user-defined literal be _dyno?

Unnecessary copies for non-macro remote_storage interfaces

I noticed an unexpected behaviour (at least for me) when comparing examples provided in readme. I basically defined drawable interface "manually" and drawableWithMacro with DYNO_INTERFACE macro.

Here is the code:

#include <dyno.hpp>
#include <iostream>
using namespace dyno::literals;
using namespace std;

struct Drawable : decltype(dyno::requires(
  "draw"_s = dyno::method<void (std::ostream&) const>
)) { };

template <typename T>
auto const dyno::default_concept_map<Drawable, T> = dyno::make_concept_map(
  "draw"_s = [](T const& self, std::ostream& out) { self.draw(out); }
);

struct drawable {
  template <typename T>
  drawable(T x) : poly_{x} { }

  void draw(std::ostream& out) const
  { poly_.virtual_("draw"_s)(out); }

private:
  dyno::poly<Drawable> poly_;
};

DYNO_INTERFACE(drawableWithMacro,
  (draw, void (std::ostream&) const)
);

struct A {
    A(){ cout << "A(): " << this << endl; }
    A(const A&){ cout << "A(const A&): " << this << endl; }
    A(A&&){ cout << "A(A&&): " << this << endl; }
    ~A(){ cout << "~A(): " << this << endl; }
  void draw(std::ostream& out) const { out << "Square\n"; }
};

void f(drawable const& d) {
    puts(__func__);
}

void fForMacro(drawableWithMacro const& d) {
    puts(__func__);
}

int main() {
  printf("\nf(A{}):\n");
  f(A{});

  printf("\nA a{}; f(a):\n");
  A a{}; f(a);

  printf("\nfForMacro(A{}):\n");
  fForMacro(A{});

  printf("\nA a2{}; fForMacro(a2):\n");
  A a2{}; fForMacro(a2);
  puts("");
}

The output is:

f(A{}):
A(): 0x7ffca1334a3f
A(const A&): 0x55605319fe80
f
~A(): 0x55605319fe80
~A(): 0x7ffca1334a3f

A a{}; f(a):
A(): 0x7ffca1334a3e
A(const A&): 0x7ffca1334a3f
A(const A&): 0x55605319fe80
f

~A(): 0x55605319fe80
~A(): 0x7ffca1334a3f

fForMacro(A{}):
A(): 0x7ffca1334a3f
A(A&&): 0x55605319fe80
fForMacro
~A(): 0x55605319fe80
~A(): 0x7ffca1334a3f

A a2{}; fForMacro(a2):
A(): 0x7ffca1334a3f
A(const A&): 0x55605319fe80
fForMacro
~A(): 0x55605319fe80

~A(): 0x7ffca1334a3f
~A(): 0x7ffca1334a3e

2 observations:

  1. For f(A{}) the drawable object is constructed with a copy, whereas in fForMacro(A{}) object is constructed with move semantics (as expected).
  2. f(a) makes two copies by value, whereas fForMacro(a2) makes just one copy (as expected).

It looks like a bug. Why is this unnecessary copy invoked?

Best regards!

make dyno::non_owning_storage the default

dyno::non_owning_storage should be the default for the following reasons
additional heap allocations are not being encourage which goes contrary to the manifesto of the dyno library
It matches the value based semantics expected from those working with function pointers.
It makes sense for all the reasons being proposed in the function_ref proposal.
http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2017/p0792r0.html
function_ref is the missing link and logical development of C function pointers
dyno just increases the arity of function to greater than 1

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. 📊📈🎉

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google ❤️ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.