proxy Runnable {
void operator()();
};
auto lambda = [] { puts("Lambda"); };Runnable r(lambda);r();Runnable r([] { puts("Lambda"); });
class Runnable { public: template <class Data> Runnable(Data& data) requires requires(Data& data) { { data() }; } { Implementation<Data> a(data); memcpy(&data_, &a, sizeof(Abstraction)); } Runnable() = default; Runnable(Runnable&&) = default; Runnable(const Runnable&) = default; Runnable& operator=(Runnable&&) = default; Runnable& operator=(const Runnable&) = default;
void operator()() { reinterpret_cast<Abstraction*>(&data_)->op_0(); }
private: class Abstraction { public: Abstraction(void* data) : data_(data) {} virtual void op_0() = 0; template <class T> T* get() { return static_cast<T*>(data_); }
private: void* data_; };
template <class Data> class Implementation final : public Abstraction { public: Implementation(Data& data) : Abstraction(&data) {} void op_0() override { (*get<Data>())(); } };
char data_[sizeof(Abstraction)];};
- Changing the keyword “interface” into “proxy” (thanks to Thiago Macierira, Myriachan and Jakob Riedle), and
There's no reason to have both. They are the same thing. With Concepts TS, it's just a constraint placed on types for function templates and class templates. What we're talking about here is more runtime polymorphism - but the interface we're defining is exactly the same thing. We don't need two keywords with two sets of rules.
For many people, the largest single problem using C++ is the lack of an extensive standard
library. A major problem in producing such a library is that C++ does not provide a sufficiently
general facility for defining ‘‘container classes’’ such as lists, vectors, and associative
arrays.
There are two approaches for providing such classes/types: One can either rely on dynamic typing
and inheritance like Smalltalk does, or one can rely on static typing and a facility for arguments
of type type. The former is very flexible, but carries a high runtime cost, and more importantly
defies attempts to use static type checking to catch interface errors. Therefore, the latter approach
was chosen.
- Changing the way of passing the concrete implementations by value into by reference, so that polymorphism and lifetime management are decoupled (thanks to Bengt Gustafsson).
std::function<void()> f1 = /**/;
std::function<void()> f2();
const std::function<void()> f3 = /**/;
Callable<void()> c1 = f1; // This copies f1. Internally, an lvalue object of type std::function<void()> is held.
Callable<void()>& c2 = f1; // This references f1. Internally, an lvalue reference (std::function<void()>&) is stored.
Callable<void()>&& c3 = f2(); // Internally stored as 'std::function<void()>&&'.Callable<void()> c4 = f2(); // This moves the result of f2 into c4. Internally stored as real std::function<void()> lvalue .const Callable<void()>& c4 = f3; //Internally 'const &' as handle.
// and so on...
This is an update version for another topic “Adding the Keyword “interface” in C++”
I replied to this topic, that it would be important, If we want to treat dynamic concepts identically to static concepts, that they behave identically to values of the concrete type:
Again, my answer assumed, that "proxies" or "interfaces" just introduce a lot of redundancy to concepts
and therefore tries to come up with a solution that reuses the concepts we already have.
For example, suppose we want to store some iterators (may have different types) in a container and perform some operations with them (only "operator*", "operator++" and "operator==" are used), we may write the following code with "interfaces":
template <class T>proxy It {T operator*();void operator++(); // The returned value is not requiredbool operator==(const It&);};std::vector<It<int>> vec;for (It<int> it : vec) { ... }
template <class I>concept bool Iterator() { return ranges::WeaklyIncrementable<I>() && requires(I i) { { *i } -> auto&&; // Requires: i is dereferenceable };}concept bool WeaklyIncrementable() { return ranges::Semiregular<I>() && requires(I i) { typename ranges::difference_type_t<I>; requires ranges::SignedIntegral<ranges::difference_type_t<I>>(); { ++i } -> ranges::Same<I&>; /* not required to be equality preserving */ i++; /* not required to be equality preserving */ };}template <class T>concept bool Destructible() { return requires(T t, const T ct, T* p) { { t.T() } noexcept; { &t } -> ranges::Same<T*>; /* not required to be equality preserving */ { &ct } -> ranges::Same<const T*>; /* not required to be equality preserving */ delete p; delete[] p; };}
template < class T, class... Args >concept bool __ConstructibleObject = /* exposition only */ ranges::Destructible<T>() && requires(Args&&... args) { T{ std::forward<Args>(args)... }; new T{ std::forward<Args>(args)... }; };
template < class T, class... Args >concept bool __BindableReference = /* exposition only */ std::is_reference<T>::value && requires(Args&&... args) { T( std::forward<Args>(args)... ); };
template < class T, class... Args >concept bool Constructible() { return __ConstructibleObject<T, Args...> || __BindableReference<T, Args...>;}
template < class T, class U >concept bool ConvertibleTo() { return std::is_convertible<T, U>::value; }
template <class T>concept bool MoveConstructible() { return ranges::Constructible<T, std::remove_cv_t<T>&&>() && ranges::ConvertibleTo<std::remove_cv_t<T>&&, T>();}
template <class T>concept bool CopyConstructible() { return ranges::MoveConstructible<T>() && ranges::Constructible<T, const std::remove_cv_t<T>&>() && ranges::ConvertibleTo<std::remove_cv_t<T>&, T>() && ranges::ConvertibleTo<const std::remove_cv_t<T>&, T>() && ranges::ConvertibleTo<const std::remove_cv_t<T>&&, T>();}
template < class T, class U >concept bool CommonReference() { return requires(T (&t)(), U (&u)()) { typename std::common_reference_t<T, U>; typename std::common_reference_t<U, T>; requires ranges::Same<std::common_reference_t<T, U>, std::common_reference_t<U, T>>(); std::common_reference_t<T, U>(t()); std::common_reference_t<T, U>(u()); };}
template <class T, class U>concept bool Assignable() { return ranges::CommonReference<const T&, const U&>() && requires(T&& t, U&& u) { { std::forward<T>(t) = std::forward<U>(u) } -> ranges::Same<T&>; };}
template <class T>concept bool Swappable() { return requires(T&& a, T&& b) { ranges::swap(std::forward<T>(a), std::forward<T>(b)); };}
template <class T, class U>concept bool Swappable() { return ranges::Swappable<T>() && ranges::Swappable<U>() && ranges::CommonReference<const T&, const U&>() && requires(T&& t, U&& u) { ranges::swap(std::forward<T>(t), std::forward<U>(u)); ranges::swap(std::forward<U>(u), std::forward<T>(t)); };}
template <class T>concept bool Movable() { return ranges::MoveConstructible<T>() && ranges::Assignable<T&, T>() && ranges::Swappable<T&>();}
template <class T>concept bool Copyable() { return ranges::CopyConstructible<T>() && ranges::Movable<T>() && ranges::Assignable<T&, const T&>();}
template <class T>concept bool DefaultConstructible() { return ranges::Constructible<T>() && requires(const std::size_t n) { new T[n]{}; /* not required to be equality preserving */ };}
template <class T>concept bool Semiregular() { return ranges::Copyable<T>() && ranges::DefaultConstructible<T>();}
template < class T >concept bool Integral() { return std::is_integral<T>::value; }
template < class T >concept bool SignedIntegral() { return Integral<T>() && std::is_signed<T>::value; }
template <class I>concept bool WeaklyIncrementable() { return ranges::Semiregular<I>() && requires(I i) { typename ranges::difference_type_t<I>; requires ranges::SignedIntegral<ranges::difference_type_t<I>>(); { ++i } -> ranges::Same<I&>; /* not required to be equality preserving */ i++; /* not required to be equality preserving */ };}
template <class I>concept bool Iterator() { return ranges::WeaklyIncrementable<I>() && requires(I i) { { *i } -> auto&&; // Requires: i is dereferenceable };}template <class T>concept bool Foo() { return requires(T t) { { t() }; { t.f() } -> int; }}
proxy Foo { void operator()(); int f();}template <class T>concept bool Foo() { return requires(T t1, T t2, const T t3) { t1.op_0(t2.op_1(t3.op_2())); };}A type satisfies "concept template Iterator" if and only if all the concepts above are satisfied! Shall we turn these stuff into a runtime wrapper? I suppose not, because that will introduce much runtime overhead, and I prefer to define a proxy instead. After all, only 3 member functions is required to be abstract at runtime.
Why would a compiler generate code for a runtime wrapper for a concept that isn't used as a runtime wrapper at any point in the program?If you say it'd need to do that to do runtime wrapper inheritance for all the base concepts, then this is a particularly flawed approach. (As are many OOP hierarchies.)
class Runnable { public: template <class Data> Runnable(Data& data) requires requires(Data& data) { { data() }; } { Implementation<Data> a(data); memcpy(data_, &a, sizeof(Abstraction)); } Runnable() = default; Runnable(Runnable&&) = default; Runnable(const Runnable&) = default; Runnable& operator=(Runnable&&) = default; Runnable& operator=(const Runnable&) = default;
void operator()() { reinterpret_cast<Abstraction*>(data_)->op_0(); }
private: class Abstraction { public: Abstraction(void* data) : data_(data) {} virtual void op_0() = 0; template <class T> T* get() { return static_cast<T*>(data_); }
private: void* data_; };
template <class Data> class Implementation final : public Abstraction { public: Implementation(Data& data) : Abstraction(&data) {} void op_0() override { (*get<Data>())(); } };
char data_[sizeof(Abstraction)];};Why would a compiler generate code for a runtime wrapper for a concept that isn't used as a runtime wrapper at any point in the program?If you say it'd need to do that to do runtime wrapper inheritance for all the base concepts, then this is a particularly flawed approach. (As are many OOP hierarchies.)Unlike concepts which can be treated as boolean constants at runtime, proxies are real types. The same as other types, any proxy object has a compile-time-defined data structure which requires a certain scale of memory to be constructed at runtime.It is possible that some expressions defined in a concept can be ignored when building a runtime wrapper according to the concept at compile-time, but this is only what we expect from the compiler, and no one can promise that. Besides, since a proxy is a type, the value of a proxy may be passed through compile units (definitions and functions maybe defined in different source files), and the compiler may know nothing about how the value is used in other compile units. So there are limitations for compiler optimizations.
--
You received this message because you are subscribed to the Google Groups "ISO C++ Standard - Future Proposals" group.
To unsubscribe from this group and stop receiving emails from it, send an email to std-proposal...@isocpp.org.
To post to this group, send email to std-pr...@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/2251d51a-005e-4f85-8e20-6e89a30d708f%40isocpp.org.
I'll write here in the behalf of hopefully most people.Apparently, runtime polymorphism is considered to have benefits for the language, given it can be implemented and specified properly.By that I mean, that a potential Proposal needs to adress the following concerns:
- Overhead w.r.t code size
- Overhead w.r.t compile time
- In what way can and should predefined Concepts be reused to generate abstract classes that forward functionality to concrete types? (possibly not at all?)
- Ensure Type safety across all compilation units
- Ensure proper ownership management
- Should "Static" concepts behave like "dynamic" ones only in that they operate at compile time? Why is a different approach preferable?
{adl_func(val) + int()} -> int;
ConceptName var = expr;
In a modularized codebase, whatever the code generation will be, it will generally only happen once (or perhaps a few times).
A module functionally represents an AST. If a module exports a type-erased type, then it is effectively exporting the AST for that type-erased type. Which means that it must export the code generated by the system that generated that type-erased type. And therefore, any code generation needed for that AST must have been performed. Therefore, users that include that module don't need to perform code generation for it.
class Base { public: virtual void f() = 0; virtual int g(double) = 0; virtual ~Base() {}
protected: Base() = default;};
class Derived : public Base { public: Derived() = default; void f() override; int g(double) override;};
Base* base = new Derived();base->f();base->g(3);delete base;proxy P {
void f();
int
g(double);
};
class T {
public:
void f();
int
g(double);
};
T t;
P p(t);
p.f();
p.g(3);
It would be good if your example was a little bit more elaborate than Runnable, for instance having a couple of named methods. As it stands today it seems implementable as library-only as the operator()() which is the "key feature" here drowns in all the boilerplate methods. You could also add comments to make it obvious which sections contain the methods generated from the proxy declaration. Also you should not write "here is a possible implementation" as it suggests that this is code you should write yourself (defeating the whole purpose of the proposal) but "here is the equivalent of the code that the compiler would automatically generate for the proxy MyProxy".
proxy Foo { void operator()(); double f(); void g(int);};class Foo { public: /* A template constructor */ template <class Data> // Data is the concrete type Foo(Data& data) requires // The parameter shall be passed as reference requires(Data data, int arg_0) { // The auto-generated concept corresponding to the proxy { data() }; { data.f() } -> double; { data.g(std::move(arg_0)) }; } { Implementation<Data> a(data); // A temporary value memcpy(data_, &a, sizeof(Abstraction)); // Copy the implementation data to the char array byte by byte } /* Auto-generated constructors and operators */ Foo() = default; Foo(Foo&&) = default; Foo(const Foo&) = default; Foo& operator=(Foo&&) = default; Foo& operator=(const Foo&) = default;
/* Calling the corresponding member functions with the auto-generated entries */ void operator()() { reinterpret_cast<Abstraction*>(data_)->op_0(); } double f() { return reinterpret_cast<Abstraction*>(data_)->op_1(); } void g(int arg_0) { reinterpret_cast<Abstraction*>(data_)->op_2(std::move(arg_0)); }
private: class Abstraction { public: /* Stores a type-erased pointer */ Abstraction(void* data) : data_(data) {} /* The pure virtual functions correspond to each expression declared in the proxy respectively */ virtual void op_0() = 0; virtual double op_1() = 0; virtual void op_2(int&&) = 0; // Every parameter is passed as rvalue /* A helper function */ template <class T> T* get() { return static_cast<T*>(data_); }
private: void* data_; };
template <class Data> class Implementation final : public Abstraction { public: /* Initialize the base class */ Implementation(Data& data) : Abstraction(&data) {} /* Implement the virtual functions with concrete function calls */ void op_0() override { (*get<Data>())(); } double op_1() override { return get<Data>()->f(); } void op_2(int&& arg_0) override { get<Data>()->g(std::forward<int>(arg_0)); } // The parameter shall be forward to the concrete member functions };
char data_[sizeof(Abstraction)]; // Declares a field large enough for any Implementation<T>};Regarding the reuse of Concepts for this purpose I find it very elegant to do so. I would not be so afraid of the load on the compiler to disentangle the concept hierarchy (it has to do this anyway to be able to see if a T fulfills a concept). However, I have a hard time understanding how the non-member requirements are to be translated to the corresponding proxy. For instance if we require that the type T fulfilling a concept Shiftable has a operator<<(ostream&, T), this would require the proxy to contain a virtual method with a signature like:
virtual ostream& __leftshift(ostream&);
and then have the compiler emit a call to it when it sees
proxy<Shiftable> p;
cout << p;
I guess this is doable, but it does complicate the implementation. What if there are two proxy parameters to a function (for different proxies) and then you try to multiply those values. Doesn't this create a need for multi-dispatch?
Furthermore, I'm pretty sure that a concept can define that a complying type should have a certain method without specifying the return type of the method as in:
requres requires(Data& d) { d.myFunc(); }
When we then try to create the proxy the return type of the created virtual function myFunc() is unknown!
I don't want to rule out concepts as the basis for proxies but I think it needs a lot more thinking and that not all concepts will be possible to proxy, or maybe that the rules for how to write concepts need to be revised again to make proxying all concepts possible, but maybe concepts has´ve gone too far now to be possible. Personally I don't care too much for concepts as they are going to be too cumbersome to use and will probably find little use outside the standard library. Especially with the contorted syntax of the concepts TS it was really awful but I have seen that they are cleaning up some of the mess more recently. I haven't followed these discussions too closely though.
Also I think it is possible to write requirement like: requires { typename Data::value_type; } which just says that Data should have a nested type value_type without telling what it is. This is perfectly ok in the template world but fails utterly in the proxy realm.
When it comes to lifetime handling I think that Jakob Riedle is right in that we should strive for getting the same type of semantics as for regular types, as he showed in his code box. I think this means that there must be some more magic in the proxy code as it will optionally own the data itself. The sad part is that now it becomes hard to see how shared_ptr<T> and shared_ptr<MyProxy> could co-exist pointing to the same T object (T fulfilling MyProxy of course). But this may be a necessary sacrifice as we can't support all user written shared pointers out there anyway. Or maybe it can be supported (similarly to how a shared_ptr<BASE> can be assigned from a shared_ptr<DERIVED> and they still share the refcount. This will require some surgery in the shared_ptr class I suspect. Thinking about the details of this will reveal new insights, no doubt.
Object* object = new Object(...); // The data to be used asynchronouslyP p(*object); // Declaring a proxycon::async_concurrent_invoke([=] { delete object; }, /* some concurrent callers */);Finally, if concepts are to be involved as the way to specify contents of proxies we could maybe solve the syntactical problems using a "magic" library template which could be called dynamic, box, proxy or something. In general I don't like mixing up library with language, but as it seems very hard to figure out a useful syntax in another way here we go:
std::box<Callable<void()>> myFunction; // Works essentially as std::functionstd::box<MyConcept> x;Then we can have two functions:void Func(MyConcept& p); // This is implicitly a template function as MyConcept is a concept (using terse template syntax)void Func(std::box<MyConcept>& p); // This is a regular function taking a magic box wrapping any object complying to MyConcept.
However, I don't think box is the best name as it doesn't really do _exactly_ what boxing in other languages does.
Look on current code bases, all code in .h file can be changed to modules. This mean all instantiation of templates that currently was done in headers will be done only once. Instantiation in .cpp will be repeated for each separate file. Even if module need multiple translation units to compile itself then we will only pay for them not for all translation units that use that module. You could even crate small auxiliary modules that will instantiation one class that will be used in other modules.
To clarify the motivation and scope, Mr. Bengt Gustafsson and I have discussed about this feature via email yesterday. Our opinions and understanding are summarized as follows.What do we have nowBefore the “proxies”, the most widely adopted method to implement polymorphism in C++ is to use virtual functions, which exist from the very beginning, and there is little update on this feature so far.We used to define base classes with pure virtual functions, define derived classes inheriting the base, and instantiate derived classes with dynamic memory allocation, as is shown below:class Base {public:virtual void f() = 0;virtual int g(double) = 0;virtual ~Base() {}protected:Base() = default;};class Derived : public Base {public:Derived() = default;void f() override;int g(double) override;};Base* base = new Derived();base->f();base->g(3);delete base;
What is the “proxies”It is obvious that dynamic memory allocation is harmful to performance, but why are we using it in the code above? Because, as we are using polymorphism with a uniform base class, the implementation for the derived class is completely unknown, including the memory required to construct the derived class (sizeof(Derived)).
Derived d{};
Base *base = &d;
//etc.I have been trying to find out whether there is a better solution, and I found it helpful to decouple the interfaces (the base classes) and the implementations (the derived classes) with the “proxies”, which is an update version for virtual functions, as is shown below.The code above can be reconstructed with the “proxies”, as is shown below:proxy P {
void f();
int g(double);
};
class T {
public:
void f();
int g(double);
};
T t;
P p(t);
p.f();
p.g(3);
Everything is so natural! The implementation only need to care about its own semantics ignoring how it corresponds with the requirements (no overriding any more), and no dynamic memory allocation happens at all!
Because the “proxies” is an update version for virtual functions, it can proxy any expression that can be declared virtual, including the overloads and operators, but is not able to proxy the expressions that cannot be declared virtual, e.g. inner types, constructors.
Ensure proper ownership managementAs Mr. Bengt Gustafsson wrote in the mail:When it comes to lifetime handling I think that Jakob Riedle is right in that we should strive for getting the same type of semantics as for regular types, as he showed in his code box. I think this means that there must be some more magic in the proxy code as it will optionally own the data itself. The sad part is that now it becomes hard to see how shared_ptr<T> and shared_ptr<MyProxy> could co-exist pointing to the same T object (T fulfilling MyProxy of course). But this may be a necessary sacrifice as we can't support all user written shared pointers out there anyway. Or maybe it can be supported (similarly to how a shared_ptr<BASE> can be assigned from a shared_ptr<DERIVED> and they still share the refcount. This will require some surgery in the shared_ptr class I suspect. Thinking about the details of this will reveal new insights, no doubt.This is a question I've been thinking about over and over again. On the one hand, if the lifetime issue is coupled with the “proxies”, it violates the single responsibility principle, and it becomes unfriendly to “stack objects”, whose lifetime is controlled by the execution of program, but it seems to be easier to use. On the other hand, if the lifetime issue is decoupled from the “proxies”, users are responsible for managing the lifetime, however, they are free to specify the algorithms (for “heap objects”, from new/delete to GC).After some research, I found it unnecessary to couple the lifetime issue with the “proxies” at all, because most situations that require polymorphism are implementable without dynamic memory allocation, as the example mentioned earlier demonstrates.
//Value semantics by default
proxy val_proxy
{
};
//Value semantics, move-only.
proxy move_proxy
{
move_proxy (const move_proxy &) = delete;
move_proxy (move_proxy&&) = default;
move_proxy &operator=(const move_proxy &) = delete;
move_proxy &operator=(move_proxy&&) = default;
};
//Has reference semantics
proxy ref_proxy reference
{
};
class shared_proxy_T
{
public:
shared_proxy_T(shared_ptr<T>); //Fill in `t_`.
//Add proxy requirements, forwarded to `t_`;
private:
shared_ptr<T> t_;
};Things your design is potentially lacking (which aren't covered in the meat of my reply):
Proxies need an equivalent of `dynamic_cast`. Or rather, an equivalent of `any_cast`, which is effectively the same thing. You need to be able to extract the type back out of the proxy. And it must be the exact type you provided the proxy.
Also, I think we need proxy maps. Remember the old C++11 concepts with concept maps, which were basically ways to coerce a type to fit a concept it doesn't really fit? I think this would be very useful for proxies.
A proxy map is a type which implements a particular proxy for a particular given type(s), as defined as a template. Consider a callable proxy. That works well enough for class types, but what if you want your callable to be able to use function pointers or member pointers? You need some kind of class type to do the translation there.
proxy Runnable { void operator()();};
void fun();
Runnable r(fun);r();That would be the point of the proxy map. For a given proxy and for a given type (or range of types), you create a class. When a user tries to turn one of those types into that class, it results in the construction of a proxy map type with that value, which is then passed into the proxy type.
Unless you can come up with a more elegant way to handle non-class types, or types that don't quite fit a proxy.
Basically, my litmus test for proxy design is this: the design is deficient if you cannot implement `std::function` by using a proxy.
/* some tasks are defined here*/auto task_a = [] { puts("Lambda"); };void task_b();auto task_c = [&] { /* do something with "stack objects" */ };
/* Sync Concurrent Invoke */con::sync_concurrent_invoke([] { puts("Calling thread does nothing but wait"); },
/* some concurrent callers that may submit tasks as proxies to some threadpools */);// Sync Concurrent Invoke will block until all the tasks are done.Because the “proxies” is an update version for virtual functions, it can proxy any expression that can be declared virtual, including the overloads and operators, but is not able to proxy the expressions that cannot be declared virtual, e.g. inner types, constructors.
No.
We're talking about a language feature. You should not limit the behavior of a language feature simply because the analogy no longer fits. If a "proxy" genuinely cannot be implemented which can forward certain constructs, so be it. But you shouldn't limit the design just to things that can be declared with virtual functions simply because it doesn't fit into the analogy you built for it.
That doesn't mean we have to do it through concepts, of course. But the idea that you can't have a proxy include non-member functions just because you can't have non-member virtual functions is needlessly limited.
Ensure proper ownership managementAs Mr. Bengt Gustafsson wrote in the mail:When it comes to lifetime handling I think that Jakob Riedle is right in that we should strive for getting the same type of semantics as for regular types, as he showed in his code box. I think this means that there must be some more magic in the proxy code as it will optionally own the data itself. The sad part is that now it becomes hard to see how shared_ptr<T> and shared_ptr<MyProxy> could co-exist pointing to the same T object (T fulfilling MyProxy of course). But this may be a necessary sacrifice as we can't support all user written shared pointers out there anyway. Or maybe it can be supported (similarly to how a shared_ptr<BASE> can be assigned from a shared_ptr<DERIVED> and they still share the refcount. This will require some surgery in the shared_ptr class I suspect. Thinking about the details of this will reveal new insights, no doubt.This is a question I've been thinking about over and over again. On the one hand, if the lifetime issue is coupled with the “proxies”, it violates the single responsibility principle, and it becomes unfriendly to “stack objects”, whose lifetime is controlled by the execution of program, but it seems to be easier to use. On the other hand, if the lifetime issue is decoupled from the “proxies”, users are responsible for managing the lifetime, however, they are free to specify the algorithms (for “heap objects”, from new/delete to GC).After some research, I found it unnecessary to couple the lifetime issue with the “proxies” at all, because most situations that require polymorphism are implementable without dynamic memory allocation, as the example mentioned earlier demonstrates.
Really? Because `std::function` disagrees with you. `std::function` is functionally a proxy for a type that is callable with the `operator()` overload specified by the `function`'s template parameter. And yet, it also requires copyable value semantics.
So clearly there is a genuine need for type-erasure+value semantics. Your personal use cases may not need it, but if `function` (and `any` for that matter) are any indication, combining the two is something people really do use.
As you think copying the wrapped object is sometimes necessary, could you provide a meaningful use case which is inconvenient to implement with the proxy or may reduce runtime overhead while implemented with std::function?
std::function<void()> f1 = /**/;
std::function<void()> f2();
const std::function<void()> f3 = /**/;
Callable<void()> c1 = f1; // This copies f1. Internally, an lvalue object of type std::function<void()> is held.
Callable<void()>& c2 = f1; // This references f1. Internally, an lvalue reference (std::function<void()>&) is stored.
Callable<void()>&& c3 = f2(); // Internally stored as 'std::function<void()>&&'.Callable<void()> c4 = f2(); // This moves the result of f2 into c4. Internally stored as real std::function<void()> lvalue .const Callable<void()>& c4 = f3; //Internally 'const &' as handle.
// and so on...
Things your design is potentially lacking (which aren't covered in the meat of my reply):
Proxies need an equivalent of `dynamic_cast`. Or rather, an equivalent of `any_cast`, which is effectively the same thing. You need to be able to extract the type back out of the proxy. And it must be the exact type you provided the proxy.
Also, I think we need proxy maps. Remember the old C++11 concepts with concept maps, which were basically ways to coerce a type to fit a concept it doesn't really fit? I think this would be very useful for proxies.
A proxy map is a type which implements a particular proxy for a particular given type(s), as defined as a template. Consider a callable proxy. That works well enough for class types, but what if you want your callable to be able to use function pointers or member pointers? You need some kind of class type to do the translation there.A function can be regarded as a const reference to a pointer, so it is OK to write the code as shown below:proxy Runnable {void operator()();};void fun();Runnable r(fun);r();
That would be the point of the proxy map. For a given proxy and for a given type (or range of types), you create a class. When a user tries to turn one of those types into that class, it results in the construction of a proxy map type with that value, which is then passed into the proxy type.
Unless you can come up with a more elegant way to handle non-class types, or types that don't quite fit a proxy.
Basically, my litmus test for proxy design is this: the design is deficient if you cannot implement `std::function` by using a proxy.I think std::function can be replaced by the proxy in many situations with less type requirements (types are not required to be CopyConstructible) and lower runtime overhead (as I roughly tested, std::function<void()> is approximately 3 times slower to construct than the proxy Runnable with ordinary function pointers on my PC).
Runnable func(std::mem_fn(&Typename::MemFunc));The reason why std::function requires the concrete implementation to be CopyConstructible is perhaps that polymorphism is usually accompanied by the use of containers (otherwise, we can use template), and containers often requires the specified types to be CopyConstructible, thus std::function requires the types it wraps to be CopyConstructible.
Because the “proxies” is an update version for virtual functions, it can proxy any expression that can be declared virtual, including the overloads and operators, but is not able to proxy the expressions that cannot be declared virtual, e.g. inner types, constructors.
No.
We're talking about a language feature. You should not limit the behavior of a language feature simply because the analogy no longer fits. If a "proxy" genuinely cannot be implemented which can forward certain constructs, so be it. But you shouldn't limit the design just to things that can be declared with virtual functions simply because it doesn't fit into the analogy you built for it.
That doesn't mean we have to do it through concepts, of course. But the idea that you can't have a proxy include non-member functions just because you can't have non-member virtual functions is needlessly limited.I wrote this only to limit the expressions that shall appear in a proxy definition. Particularly, an expression in the implementation is not necessary to be a member function, it can even be static. It is defined that a proxy can and can only proxy the expressions that are able to be declared virtual, because C++ has done a lot in the virtual function mechanism, and there is enough research in what expressions are suitable for runtime polymorphism.
Ensure proper ownership managementAs Mr. Bengt Gustafsson wrote in the mail:When it comes to lifetime handling I think that Jakob Riedle is right in that we should strive for getting the same type of semantics as for regular types, as he showed in his code box. I think this means that there must be some more magic in the proxy code as it will optionally own the data itself. The sad part is that now it becomes hard to see how shared_ptr<T> and shared_ptr<MyProxy> could co-exist pointing to the same T object (T fulfilling MyProxy of course). But this may be a necessary sacrifice as we can't support all user written shared pointers out there anyway. Or maybe it can be supported (similarly to how a shared_ptr<BASE> can be assigned from a shared_ptr<DERIVED> and they still share the refcount. This will require some surgery in the shared_ptr class I suspect. Thinking about the details of this will reveal new insights, no doubt.This is a question I've been thinking about over and over again. On the one hand, if the lifetime issue is coupled with the “proxies”, it violates the single responsibility principle, and it becomes unfriendly to “stack objects”, whose lifetime is controlled by the execution of program, but it seems to be easier to use. On the other hand, if the lifetime issue is decoupled from the “proxies”, users are responsible for managing the lifetime, however, they are free to specify the algorithms (for “heap objects”, from new/delete to GC).After some research, I found it unnecessary to couple the lifetime issue with the “proxies” at all, because most situations that require polymorphism are implementable without dynamic memory allocation, as the example mentioned earlier demonstrates.
Really? Because `std::function` disagrees with you. `std::function` is functionally a proxy for a type that is callable with the `operator()` overload specified by the `function`'s template parameter. And yet, it also requires copyable value semantics.
So clearly there is a genuine need for type-erasure+value semantics. Your personal use cases may not need it, but if `function` (and `any` for that matter) are any indication, combining the two is something people really do use.As you think copying the wrapped object is sometimes necessary, could you provide a meaningful use case which is inconvenient to implement with the proxy or may reduce runtime overhead while implemented with std::function?
template <class P /*, class Allocator = std::allocator<void>*/> // Maybe some allocator is allowed hereclass Wrapper { public: template <class Data> Wrapper(Data data) : data_(new Implementation<Data>(std::move(data))), p_(static_cast<Implementation<Data>*>(data_.get())->get()) {}
Wrapper(const Wrapper& rhs) : data_(rhs.data_.get() == nullptr ? nullptr : rhs.data_->clone()), // The new data is a copy from the original data with polymorphism p_(rhs.p_) {}
Wrapper() = default; Wrapper(Wrapper&&) = default;
/* Access to the proxy */ P& get() { return p_; } operator P& () { return p_; }
private: class Abstraction { public: /* Pure virtual methods to construct and destruct */ virtual Abstraction* clone() = 0; virtual ~Abstraction() {} };
template <class Data> class Implementation : public Abstraction { public: Implementation(Data&& data) : data_(std::forward<Data>(data)) {} Implementation(const Data& data) : data_(data) {}
Data& get() { return data_; }
Abstraction* clone() override { return new Implementation(data_); }
private: Data data_; };
/* The two data structures hold distinct pointers */ std::unique_ptr<Abstraction> data_; P p_;};auto f = std::bind(...);Runnable r(f);Dear Mr. Bengt Gustafsson and Mr. Nicol Bolas,Thank you for your ideas.Such requirements really do exist! But I think this is more appropriate to be a library feature rather than a lanuage feature to bind a value to a proxy. I have just built a "Wrapper" class, as is shown below:
[...]
With this class, it is possible to use "Wrapper<SomeProxy>" to turn a proxy into a value wrapper. But as it is a wrapper (like "std::reference_wrapper" does), certain member functions are provided to access the proxy ("get()" and "operator P&").I think this is enough so far.
P.S. It is possible for a proxy to proxy any type, including member functions, as long as they are not temporary variables:auto f = std::bind(...);Runnable r(f);
On Saturday, May 13, 2017 at 10:24:29 PM UTC-4, Mingxin Wang wrote:Dear Mr. Bengt Gustafsson and Mr. Nicol Bolas,Thank you for your ideas.Such requirements really do exist! But I think this is more appropriate to be a library feature rather than a lanuage feature to bind a value to a proxy. I have just built a "Wrapper" class, as is shown below:[...]With this class, it is possible to use "Wrapper<SomeProxy>" to turn a proxy into a value wrapper. But as it is a wrapper (like "std::reference_wrapper" does), certain member functions are provided to access the proxy ("get()" and "operator P&").I think this is enough so far.
The deficiencies relative to a hand-written type-erased type are readily apparent:
1) The wrapper takes up more memory.
sizeof(std::function<void()>) == 32;sizeof(Wrapper<Runnable>) == 24;2) The wrapper is less efficient (since it has two type-erasures instead of just one).
void small_demo(); // An ordinary function
constexpr int UPPER = 10000000;
for (int i = 0; i < UPPER; ++i) { std::function<void()> f(small_demo);}void small_demo(); // An ordinary function
constexpr int UPPER = 10000000;
for (int i = 0; i < UPPER; ++i) { Wrapper<Runnable> f(small_demo);}class LargeDemo { // A large enough functor that is not able to be stored in a std::function<void> public: void operator()() {}
private: int a[1000];};
constexpr int UPPER = 10000000;
for (int i = 0; i < UPPER; ++i) { std::function<void()> f(large_demo);}class LargeDemo { // A large enough functor that is not able to be stored in a std::function<void> public: void operator()() {}
private: int a[1000];};
constexpr int UPPER = 10000000;
for (int i = 0; i < UPPER; ++i) { Wrapper<Runnable> f(large_demo);}3) The wrapper is far less convenient to actually use, since it's not the actual proxy; you have to use `get` or an implicit conversion to get at the behavior you want.
If it would be better for a user to hand-write a type-erased type rather than use your language-based one, then your language feature has design problems.
Is not the whole point of this feature to make type-erasure a first-class part of the language? If so, then it needs to be usable by everyone who wants to use type-erasure, not just everyone who wants reference semantics. Concepts doesn't have this flaw; if you want to pass a value that satisfies a concept around by value, you can (so long as the concept allows such copying/moving).
Why can't proxies?P.S. It is possible for a proxy to proxy any type, including member functions, as long as they are not temporary variables:auto f = std::bind(...);Runnable r(f);
But now you have to make sure that `f` stays alive so long as `r` does. With regular function pointers, you don't have to care; function pointers are valid forever. As are member pointers, but those don't work here.
If you were to write a hand-written `Runnable<Func>` type, even if it only has reference semantics, you would still give it native support for member pointers. Because those are genuine "runnables" and should have first-class access like function pointers.
On Sunday, May 14, 2017 at 10:56:16 AM UTC+8, Nicol Bolas wrote:On Saturday, May 13, 2017 at 10:24:29 PM UTC-4, Mingxin Wang wrote:Dear Mr. Bengt Gustafsson and Mr. Nicol Bolas,Thank you for your ideas.Such requirements really do exist! But I think this is more appropriate to be a library feature rather than a lanuage feature to bind a value to a proxy. I have just built a "Wrapper" class, as is shown below:[...]With this class, it is possible to use "Wrapper<SomeProxy>" to turn a proxy into a value wrapper. But as it is a wrapper (like "std::reference_wrapper" does), certain member functions are provided to access the proxy ("get()" and "operator P&").I think this is enough so far.
The deficiencies relative to a hand-written type-erased type are readily apparent:
1) The wrapper takes up more memory.On my compiler (G++ 6.3.0 x64):sizeof(std::function<void()>) == 32;sizeof(Wrapper<Runnable>) == 24;
2) The wrapper is less efficient (since it has two type-erasures instead of just one).To test the performance of the wrapper,
I did a little experiment on my PC (with G++ 6.3.0 x64, Command line: g++ -march=corei7-avx -O2 -Wall -std=c++17):
<snip>
But as you can see from the table, Wrapper<Runnable> is a little more efficient than std::function<void> with large data sets. Besides, Wrapper<T> is just a descriptive implementation, compiler vendors may apply other algorithms to reach a compromise if necessary.
P.S. It is possible for a proxy to proxy any type, including member functions, as long as they are not temporary variables:auto f = std::bind(...);Runnable r(f);
But now you have to make sure that `f` stays alive so long as `r` does. With regular function pointers, you don't have to care; function pointers are valid forever. As are member pointers, but those don't work here.
If you were to write a hand-written `Runnable<Func>` type, even if it only has reference semantics, you would still give it native support for member pointers. Because those are genuine "runnables" and should have first-class access like function pointers.That is why I think the "proxies" should be a language feature and the "wrappers" should be a library feature. Wrappers are not proxies, they are bound from a proxy and a user-defined implementations, so they should not have semantics like the "proxies". This is similar with the reason why std::reference_wrapper do not have the semantics as the wrapped object has.
(snipped…)
On May 15, 2017, at 2:22 PM, Bengt Gustafsson <bengt.gu...@beamways.com> wrote:
Den 2017-05-15 kl. 02:55, skrev Bryce Glover:
(snipped…)
Implementing a way to hide the parts of itself that a derived class inherits from its parent base class behind a quasi-opaque, type-erasing ‘pointer to implementation’ from which it could then retrieve this information sounds like it could be interesting and could eventually end up being useful to somebody, but I’m not entirely certain how well the introduction of such a concept contributes to the discussion undertaken in this thread so far.
Well, my intention was for this to become clear towards the end of the post, where the "proxy/interface" version of the indirect inheritance feature is discussed. I already had the indirect inheritance idea back when operator.() was discussed, as an alternative way to implement the handle classes it was aiming at. The main problem with operator.() was how to handle the ambiguity if both the handle class and its "T" had a method of the same name, which was solved in a tried and true way by viewing this as inheritance (i.e. the handle class method takes precedence and hides the T method unless a T:: is prefixed at the call site).
When the proxy/interface discussion started it became obvious that the requirements were similar: A template class is to implement the API of its template parameter T by some kind of forwarding, but in this case involving a translation table.
…
…
My writeup is an attempt at showing how these two features can be stacked to achieve both goals.
…
…
Admittedly my approach is fairly different from the original proposer's who appointed the "magic features" to the new type of "thing" he called interface or proxy, while in my approach this declaration is just a regular struct/class declaration and the magic is in the indirect inheritance declaration.
…
…
Perhaps one could implement a fully functional run-time concept wrapper implementing properly forwarding versions of the functionality expected of the type of objects the compile-time concept associated with this wrapper would require it to contain using this technique, but I feel both that a run-time concept facility should be easy to use by language users with abilities nearer to beginner level than this particular iteration of this construct and that said iteration doesn’t feel quite right to me when it comes to usability and understandability, but you should probably trust other peoples’ reactions more than mine on this front, as I’m not the best person to give feedback on these kinds of matters. In any case, it strikes me that a version of this ‘indirect inheritance’ that one might consider more acceptably traditional would involve classes satisfying concepts related by subsumption, but that doesn’t really describe the model you’ve come up with.
Isn't this exactly what I describe in the first half of the writeup? This still requires that the _actual_ object referred to is a subclass of T and thus, to the best knowledge of the compiler, satisfied the Liskov substitution principle (I looked this up in Wikipedia as I didn’t know what subsumption ment).
…
…
Also, the part of your post where you started talking about maintaining object dispatch tables on top of the virtual function dispatch tables that already exist reminded me indirectly of some of the work that’s already been done to describe and even implement open multimethods, but there’s a chance that this impression of mine could be somewhat misguided.
Yes, you are mistaken, this does not solve multimethod issues, it is related to the case that the constructor src operand that the compiler thinks is a U is really a subclass of U reimplementing any or all of its virtual methods, thus requiring both a step to come from the "apparent" type T to U and a separate step from U to its subclass method (as the first step is fixed at compile time for each call site while the latter can vary for each invokation of the call site).
…
…
The additional complexity you consider at the end probably doesn’t help, either, but, again, I’m not really the best individual to go through all of this with somebody despite wanting to use it myself.
As I point out it only allows using the proxy<T> containing a U in some contexts, i.e. when the T is the “this" object.
…
…
— Bryce Glover
-- Bengt Gustafsson CEO, Beamways AB Westmansgatan 37 582 16 Linköping, Sweden +46 (705) 338259 Skype: benke_g www.beamways.com
On May 17, 2017, at 6:13 PM, std-pr...@isocpp.org wrote:Bengt Gustafsson <bengt.gu...@beamways.com>: May 16 02:46PM -0700
/* Holds a contiguous memory segment, sizeof(MemoryBlock<SIZE>) == SIZE */template <std::size_t SIZE>class MemoryBlock { public: MemoryBlock() = default; MemoryBlock(MemoryBlock&&) = default; MemoryBlock(const MemoryBlock&) = default; MemoryBlock& operator=(MemoryBlock&&) = default; MemoryBlock& operator=(const MemoryBlock&) = default;
/* Access to the memory segment */ void* get() { return data_; }
private: char data_[SIZE];};
// P: A proxy type// SSO_SIZE: The maximum size for SSO optimizationtemplate <class P, std::size_t SSO_SIZE = 16>class Wrapper { public: /* Default constructor */ Wrapper() { init(); }
/* Initialize the wrapper with a concrete data */ template <class T> Wrapper(T data) { init(std::move(data)); }
/* Copy constructor */ Wrapper(const Wrapper& rhs) { // There are two situations: // 1. rhs in invalid, // *this shall also be invalid. // 2. rhs is valid, no matter whether SSO optimization is activated, // *this shall be initialized with rhs. if (rhs.holder_ == nullptr) { init(); } else { rhs.holder_->init(*this); } }
/* Move constructor */ Wrapper(Wrapper&& lhs) { // There are three situations: // 1. lhs in invalid, // *this shall also be invalid. // 2. lhs is valid and SSO optimization is activated, // *this shall be initialized with lhs. // 3. lhs in valid and SSO optimization is inactivated, // The pointer of the holder can be simply moved from lhs to *this. if (lhs.holder_ == nullptr) { init(); } else if (lhs.holder_ == lhs.sso_block_.get()) { lhs.holder_->init(*this); } else { holder_ = lhs.holder_; lhs.holder_ = nullptr; } }
/* Destructor */ ~Wrapper() { // There are two situations: // 1. SSO optimization is activated, // The destructor of the holder shall be called without release the memory. // 2. SSO optimization is inactivated, // The pointer shall be deleted. // If holder_ is a null pointer, this operation haves no side-effect. if (holder_ == sso_block_.get()) { holder_->~AbstractHolder(); } else { delete holder_; } }
/* Access to the proxy */ P get_proxy() { return holder_->get_proxy(); }
private: /* The base class for lifetime management */ class AbstractHolder { public: /* Virtual destructor */ virtual ~AbstractHolder() {}
/* Initialize another wrapper with *this */ virtual void init(Wrapper&) = 0;
/* Get a proxy */ virtual P get_proxy() = 0; };
template <class T> class ConcreteHolder : public AbstractHolder { public: /* Constructors */ ConcreteHolder(T&& data) : data_(std::forward<T>(data)) {} ConcreteHolder(const T& data) : data_(data) {} ConcreteHolder(ConcreteHolder&&) = default; ConcreteHolder(const ConcreteHolder&) = default;
/* Initialize the wrapper with the data (copy construct) */ void init(Wrapper& wrapper) override { wrapper.init(data_); }
P get_proxy() override { return P(data_); }
private: T data_; };
/* Initialize *this to be invalid */ void init() { holder_ = nullptr; }
/* Overload for small object */ template <class T> void init(T&& data) requires (sizeof(T) <= SSO_SIZE) { // Let holder_ point to the reserved SSO block, and SSO optimization is activated holder_ = reinterpret_cast<AbstractHolder*>(sso_block_.get());
// Call the constructor of the ConcreteHolder without memory allocation new (reinterpret_cast< ConcreteHolder<std::remove_reference_t<T>>*>(sso_block_.get())) ConcreteHolder<std::remove_reference_t<T>>(std::forward<T>(data)); }
/* Overload for large object */ template <class T> void init(T&& data) requires (sizeof(T) > SSO_SIZE) { // Let holder_ point to a "new" object, and SSO optimization is inactivated holder_ = new ConcreteHolder<std::remove_reference_t<T>>(std::forward<T>(data)); }
/* Associates with the lifetime management strategy */ AbstractHolder* holder_;
/* A reserved block for SSO optimization */ MemoryBlock<sizeof(ConcreteHolder<MemoryBlock<SSO_SIZE>>)> sso_block_;};