template <class T>class Callable; // undefined
/* Interface declaration with pure virtual class */template <class R, class... Args>class Callable<R(Args...)> { public: virtual R operator()(Args... args) = 0;};
#include <system_error>
/* Auto generated specialization for proxy<Callable<R(Args...)>, W> */template <class R, class... Args, class W> requires Wrapper<W>()class proxy<Callable<R(Args...)>, W> { public: /* Construct with a value of any type */ /* More concepts may be required to check whether T is suitable for this interface */ template <class T> proxy(T&& data) requires !std::is_same<std::remove_cv_t<std::remove_reference_t<T>>, proxy>::value && requires(T t, Args&&... args) { { t(std::forward<Args>(args)...) } -> R; } { init(std::forward<T>(data)); }
/* Default constructor */ proxy() { init(); }
/* Move constructor */ proxy(proxy&& lhs) { lhs.move_init(*this); }
/* Copy constructor */ proxy(const proxy& rhs) { rhs.copy_init(*this); }
/* Destructor */ ~proxy() { deinit(); }
proxy& operator=(const proxy& rhs) { deinit(); rhs.copy_init(*this); return *this; }
proxy& operator=(proxy&& lhs) { deinit(); lhs.move_init(*this); return *this; }
template <class T> proxy& operator=(T&& data) requires !std::is_same<std::remove_cv_t<std::remove_reference_t<T>>, proxy>::value { deinit(); init(std::forward<T>(data)); return *this; }
/* Auto generated member function */ /* (Args...) != (_Args...), args... shall be forwarded to the virtual function */ template <class... _Args> R operator()(_Args&&... args) { // Call the target function with polymorphism return (*reinterpret_cast<Abstraction*>(data_.get()))(std::forward<_Args>(args)...); }
private: /* Base class, extending the original interface */ class Abstraction : public Callable<R(Args...)> { public: Abstraction() = default;
/* Initialize the wrapper */ template <class T> Abstraction(T&& data) : wrapper_(std::forward<T>(data)) {}
/* Non-virtual copy construct */ void copy_init(void* mem) const { /* Copy the pointer of the vtable */ memcpy(mem, this, sizeof(Callable<R(Args...)>));
/* Initialize the wrapper with lvalue */ new (&reinterpret_cast<Abstraction*>(mem)->wrapper_) W(wrapper_); }
void move_init(void* mem) { memcpy(mem, this, sizeof(Callable<R(Args...)>)); new (&reinterpret_cast<Abstraction*>(mem)->wrapper_) W(std::move(wrapper_)); }
W wrapper_; // A type-erased wrapper };
/* A placeholder for the uninitialized state */ class Uninitialized : public Abstraction { public: /* Only for demonstration */ R operator()(Args...) override { throw std::runtime_error("Using uninitialized proxy"); } };
/* Type-specific implementation */ template <class T> class Implementation : public Abstraction { public: template <class U> Implementation(U&& data) : Abstraction(std::forward<U>(data)) {}
R operator()(Args... args) override { /* Restore the type and call the target function */ return (*reinterpret_cast<T*>(this->wrapper_.get()))(std::forward<Args>(args)...); } };
void init() { new (reinterpret_cast<Uninitialized*>(data_.get())) Uninitialized(); }
/* Initialize with a concrete type and value */ template <class T> void init(T&& data) { new (reinterpret_cast<Implementation<std::remove_reference_t<T>>*>(data_.get())) Implementation<std::remove_reference_t<T>>(std::forward<T>(data)); }
/* Copy semantics */ void copy_init(proxy& rhs) const { // Forward this operation reinterpret_cast<const Abstraction*>(data_.get())->copy_init(rhs.data_.get()); }
/* Move semantics */ void move_init(proxy& rhs) { // Forward this operation reinterpret_cast<Abstraction*>(data_.get())->move_init(rhs.data_.get()); }
/* Destroy semantics */ void deinit() { // Forward this operation reinterpret_cast<Abstraction*>(data_.get())->~Abstraction(); }
/* sizeof(Uninitialized) == sizeof(ptrdiff_t) + sizeof(W) */ MemoryBlock<sizeof(Uninitialized)> data_;};
#include <cmath>
#include "proxy.hpp"
int main() { DeepProxy<Callable<void()>> a([] { puts("Lambda Expression 1"); }); a();
SharedProxy<Callable<int(int)>> b(&std::abs<int>); printf("%d\n", b(-2));
auto lambda_2 = [] { puts("Lambda Expression 2"); }; DefferedProxy<Callable<void()>> c(lambda_2); c(); c = a; c();
return 0;}
Technical SpecificationsWrapper requirementsA type W meets the Wrapper requirements if the following expressions are well-formed and have the specific semantics (w denotes a value of type W).w.get()Requires: w is initialized with an object.Effects: acquires the pointer of the wrapped object if there is one.Return type: void*.Returns: a pointer of the wrapped object.
Class template proxyExpression "proxy<I, W>" is a well-formed type if I is a pure virtual class (without a virtual destructor) and W is a type meets the Wrapper requirements defined above."proxy<I, W>" is MoveConstructible if W is MoveConstructible, while "proxy<I, W>" is CopyConstructible if W is CopyConstructible.Providing p is a value of "proxy<I, W>" and i is a pointer of I, "p.f(args...)" shall be a valid expression if "(*i).f(args...)" is a valid expression, where f is any valid function name (including operator overloads) and "args..." is any valid combination of values of any type.
Prototypes for WrappersClass "SharedWrapper" (with shared semantics), class template "DeepWrapper" (with value semantics and SOO feature) and class "DefferedWrapper" (with reference semantics) are designed to meet the Wrapper requirements. Possible implementation is included in the attachments.
On Tuesday, June 13, 2017 at 9:49:43 AM UTC-4, Mingxin Wang wrote:Technical SpecificationsWrapper requirementsA type W meets the Wrapper requirements if the following expressions are well-formed and have the specific semantics (w denotes a value of type W).w.get()Requires: w is initialized with an object.Effects: acquires the pointer of the wrapped object if there is one.Return type: void*.Returns: a pointer of the wrapped object.
This seems decidedly thin on details.
As I understand your design, the "wrapper" provides both the storage for the object/pointer as well as determining the semantics relative to that object/pointer. This is not entirely clear from your technical specifications, but everything else I'm going to say is based on this assumption.
First, "wrapper" is a terrible name for this concept. It isn't "wrapping" anything; it's providing storage and specifying the semantics of the proxy (which also isn't a particularly good name). That isn't "wrapping". It's simply providing the semantics of the type. So "Semantics" seems a much more descriptive name.
Second, the `get` interface is entirely insufficient to fully define this concept. The "wrapper" needs to be able to be initialized from any object that matches `I`, so that has to be part of its interface. Your example code shows this, but these "Wrapper requirements" don't mention it. Furthermore:
Third, it's not clear who is actually responsible for the type erasure part of this. The type `T` provided to a "proxy" seems to be erased by the code generated data. But since the "wrapper" holds the storage for said `T`, it must also have enough information to destroy that object. After all, the "wrapper" may copy/move from the given `T` into new storage for that `T`. Which means that the "wrapper" implementation has to know what that `T` was when it goes to destroy it.
This means that any "wrapper" that actually owns a `T` must itself perform type-erasure, for the purpose of destroying the `T`. We discussed this in your last thread. Double-type-erasure is less efficient than single-type-erasure, thus violating one of your design goals.
Now, you might be able to work around this problem. You could design it so that the "wrapper" explicitly advertises in its interface that it is an "owning" wrapper, such that it needs to be provided with the un-erased `T` when the "proxy" is being destroyed. But that's a much more complex interface than you have outlined here.
Class template proxyExpression "proxy<I, W>" is a well-formed type if I is a pure virtual class (without a virtual destructor) and W is a type meets the Wrapper requirements defined above."proxy<I, W>" is MoveConstructible if W is MoveConstructible, while "proxy<I, W>" is CopyConstructible if W is CopyConstructible.Providing p is a value of "proxy<I, W>" and i is a pointer of I, "p.f(args...)" shall be a valid expression if "(*i).f(args...)" is a valid expression, where f is any valid function name (including operator overloads) and "args..." is any valid combination of values of any type.
As previously mentioned, the "proxy" ought to provide `any_cast`-like functionality. That is, the ability to extract an object/reference to the exact `T` which was provided. Since the "proxy" type's semantics are part of its type declaration (defined by `W`), it would be easy to specialize the interface for reference proxies vs. value proxies.
Alternatively, you could use the `std::function::target` interface for a lighter-weight version. But the overall point is the same: a type-erased type ought to be able to extract what it was given.
template<class ValueType, class I, class W>ValueType proxy_cast(const proxy<I, W>& operand);
template<class ValueType, class I, class W>ValueType proxy_cast(proxy<I, W>& operand);
template<class ValueType, class I, class W>ValueType proxy_cast(proxy<I, W>&& operand);
template<class ValueType, class I, class W>const ValueType* proxy_cast(const proxy<I, W>* operand) noexcept;
template<class ValueType, class I, class W>ValueType* proxy_cast(proxy<I, W>* operand) noexcept;
template <class I, class W> requires Wrapper<W>()class proxy<I, W> { public: // ... template <class T> T* __cast() { Abstraction* ptr = dynamic_cast<Implementation<T>*>(reinterpret_cast<Abstraction*>(data_.get())); return ptr == nullptr ? nullptr : reinterpret_cast<T*>(ptr->wrapper_.get()); } // ... private: class Abstraction : public I { public: /* ... */ W wrapper_; }; class Uninitialized : public Abstraction { /* ... */ }; template <class T> class Implementation : public Abstraction { /* ... */ }; MemoryBlock<sizeof(Uninitialized)> data_;};
Prototypes for WrappersClass "SharedWrapper" (with shared semantics), class template "DeepWrapper" (with value semantics and SOO feature) and class "DefferedWrapper" (with reference semantics) are designed to meet the Wrapper requirements. Possible implementation is included in the attachments.
"DeepWrapper" is the wrong name. It provides "value semantics". Calling it "deep" suggests "deep copying", which is not what this provides. It should simply be "ValueWrapper". And there should also be a "MoveValueWrapper", for use in cases where you want to allow users to provide move-only types.
"DefferedWrapper" is similarly misnamed (though also misspelled). It provides "reference semantics", so it is a "ReferenceWrapper". Admittedly, we already have a type with that name, but that's another reason not to call them "wrappers" at all.
Also, it's not clear what "SharedWrapper" means. Does it copy/move into memory owned by the proxy, ala `make_shared`? Can you pass it a `shared_ptr<T>` instead of a `T`, so that you can share ownership of the object with the proxy? If not, why not? If you're going to allow shared ownership of the value, then it is just as reasonable to allow shared ownership of the value with objects that aren't the proxy.
On Tuesday, June 13, 2017 at 11:20:26 PM UTC+8, Nicol Bolas wrote:On Tuesday, June 13, 2017 at 9:49:43 AM UTC-4, Mingxin Wang wrote:Technical SpecificationsWrapper requirementsA type W meets the Wrapper requirements if the following expressions are well-formed and have the specific semantics (w denotes a value of type W).w.get()Requires: w is initialized with an object.Effects: acquires the pointer of the wrapped object if there is one.Return type: void*.Returns: a pointer of the wrapped object.
This seems decidedly thin on details.
As I understand your design, the "wrapper" provides both the storage for the object/pointer as well as determining the semantics relative to that object/pointer. This is not entirely clear from your technical specifications, but everything else I'm going to say is based on this assumption.A "Wrapper" is only required to support semantics for addressing (at minimum).
First, "wrapper" is a terrible name for this concept. It isn't "wrapping" anything; it's providing storage and specifying the semantics of the proxy (which also isn't a particularly good name). That isn't "wrapping". It's simply providing the semantics of the type. So "Semantics" seems a much more descriptive name.I think "Semantics" is not as good as "Wrapper". As this is a relatively subjective issue, maybe we need more feedback and discuss about it later.Second, the `get` interface is entirely insufficient to fully define this concept. The "wrapper" needs to be able to be initialized from any object that matches `I`, so that has to be part of its interface. Your example code shows this, but these "Wrapper requirements" don't mention it. Furthermore:There is no particular requirements on "what type can be used to construct a wrapper". Actually, a type that meets the Wrapper requirements may carry such constraints on its constructors.
Third, it's not clear who is actually responsible for the type erasure part of this. The type `T` provided to a "proxy" seems to be erased by the code generated data. But since the "wrapper" holds the storage for said `T`, it must also have enough information to destroy that object. After all, the "wrapper" may copy/move from the given `T` into new storage for that `T`. Which means that the "wrapper" implementation has to know what that `T` was when it goes to destroy it.A Wrapper may be responsible to destroy an object just as std::any does, and a Proxy is responsible for ACCESSING the object without RTTI.This means that any "wrapper" that actually owns a `T` must itself perform type-erasure, for the purpose of destroying the `T`. We discussed this in your last thread. Double-type-erasure is less efficient than single-type-erasure, thus violating one of your design goals.Not exactly. Double-type-erasure only requires a little more memory (one-pointer size) and is more compatible.
That is why we usually use "combination instead of inheritance" in architecture designing. Besides, I have ran a performance test for DeepWrapper<Callable<void()>, 16u> and std::function<void()>
which have the same SOO size on my compiler (Windows 7 x64, gcc version 6.3.0 x86_64-posix-seh-rev2, Built by MinGW-W64 project, Command: g++.exe -march=corei7-avx -O2 -m64 -fconcepts -std=c++17), and the result turns out to be positive that DeepWrapper<Callable<void()>, 16u> is more efficient than the implementation of std::function<void()> most of the time because there is an extra addressing operation for the virtual table for std::function<void()>, as is shown below (x: the size of the object for type erasure; y: the time elapsed):
Now, you might be able to work around this problem. You could design it so that the "wrapper" explicitly advertises in its interface that it is an "owning" wrapper, such that it needs to be provided with the un-erased `T` when the "proxy" is being destroyed. But that's a much more complex interface than you have outlined here.
This is not always necessary, especially when W is a trivial type.
Class template proxyExpression "proxy<I, W>" is a well-formed type if I is a pure virtual class (without a virtual destructor) and W is a type meets the Wrapper requirements defined above."proxy<I, W>" is MoveConstructible if W is MoveConstructible, while "proxy<I, W>" is CopyConstructible if W is CopyConstructible.Providing p is a value of "proxy<I, W>" and i is a pointer of I, "p.f(args...)" shall be a valid expression if "(*i).f(args...)" is a valid expression, where f is any valid function name (including operator overloads) and "args..." is any valid combination of values of any type.
On Wednesday, June 14, 2017 at 4:11:55 AM UTC-4, Mingxin Wang wrote:On Tuesday, June 13, 2017 at 11:20:26 PM UTC+8, Nicol Bolas wrote:On Tuesday, June 13, 2017 at 9:49:43 AM UTC-4, Mingxin Wang wrote:Technical SpecificationsWrapper requirementsA type W meets the Wrapper requirements if the following expressions are well-formed and have the specific semantics (w denotes a value of type W).w.get()Requires: w is initialized with an object.Effects: acquires the pointer of the wrapped object if there is one.Return type: void*.Returns: a pointer of the wrapped object.
This seems decidedly thin on details.
As I understand your design, the "wrapper" provides both the storage for the object/pointer as well as determining the semantics relative to that object/pointer. This is not entirely clear from your technical specifications, but everything else I'm going to say is based on this assumption.A "Wrapper" is only required to support semantics for addressing (at minimum).First, "wrapper" is a terrible name for this concept. It isn't "wrapping" anything; it's providing storage and specifying the semantics of the proxy (which also isn't a particularly good name). That isn't "wrapping". It's simply providing the semantics of the type. So "Semantics" seems a much more descriptive name.I think "Semantics" is not as good as "Wrapper". As this is a relatively subjective issue, maybe we need more feedback and discuss about it later.Second, the `get` interface is entirely insufficient to fully define this concept. The "wrapper" needs to be able to be initialized from any object that matches `I`, so that has to be part of its interface. Your example code shows this, but these "Wrapper requirements" don't mention it. Furthermore:There is no particular requirements on "what type can be used to construct a wrapper". Actually, a type that meets the Wrapper requirements may carry such constraints on its constructors.
Which means that there is a requirement that the "wrapper" is constructible from a reference to some non-zero set of types, yes?
Also, if important aspects of the "wrapper"'s interface are not "requirements", can we get a section that details all of the optional parts of its interface too?
Third, it's not clear who is actually responsible for the type erasure part of this. The type `T` provided to a "proxy" seems to be erased by the code generated data. But since the "wrapper" holds the storage for said `T`, it must also have enough information to destroy that object. After all, the "wrapper" may copy/move from the given `T` into new storage for that `T`. Which means that the "wrapper" implementation has to know what that `T` was when it goes to destroy it.A Wrapper may be responsible to destroy an object just as std::any does, and a Proxy is responsible for ACCESSING the object without RTTI.This means that any "wrapper" that actually owns a `T` must itself perform type-erasure, for the purpose of destroying the `T`. We discussed this in your last thread. Double-type-erasure is less efficient than single-type-erasure, thus violating one of your design goals.Not exactly. Double-type-erasure only requires a little more memory (one-pointer size) and is more compatible.
Since you defined "efficiency" as "performance", OK. But it is also misleading, since "efficiency" means more than just runtime performance. So while you follow the letter of your statement, the apparent spirit of it (that a user should gain nothing from hand-writing their own) is still violated.
Also, it's not clear what you mean by "is more compatible". With what would it be "compatible"?
That is why we usually use "combination instead of inheritance" in architecture designing. Besides, I have ran a performance test for DeepWrapper<Callable<void()>, 16u> and std::function<void()>
That's the wrong test. A proper test would be between "DeepWrapper" and a hand-crafted equivalent. `std::function` has additional stuff in it that has no analog in the "DeepWrapper" proxy.
Furthermore, a comprehensive test should examine the size difference between the two objects, as well as looking at the generated code with an eye to code size (type-erasure tends to induce bloat).
which have the same SOO size on my compiler (Windows 7 x64, gcc version 6.3.0 x86_64-posix-seh-rev2, Built by MinGW-W64 project, Command: g++.exe -march=corei7-avx -O2 -m64 -fconcepts -std=c++17), and the result turns out to be positive that DeepWrapper<Callable<void()>, 16u> is more efficient than the implementation of std::function<void()> most of the time because there is an extra addressing operation for the virtual table for std::function<void()>, as is shown below (x: the size of the object for type erasure; y: the time elapsed):Now, you might be able to work around this problem. You could design it so that the "wrapper" explicitly advertises in its interface that it is an "owning" wrapper, such that it needs to be provided with the un-erased `T` when the "proxy" is being destroyed. But that's a much more complex interface than you have outlined here.This is not always necessary, especially when W is a trivial type.
I'm not sure I understand your point here.
As I understand this feature, the whole point of "wrappers" is to allow support for non-reference semantics. Any wrappers that provide non-reference semantics would have to be non-trivial. So what does it matter if a more complex interface isn't needed for trivial "wrappers"? Many of the non-trivial cases could really use that interface, and support for those cases is exactly why "wrapper" exists.
Also, we're not exactly talking about a complex interface here. The only reason double-erasure is needed is because your interface between the proxy and the wrapper is based on the wrapper's special member functions. The destructor of the Wrapper is what calls the destructor of the "wrapped" object. If the Wrapper is copyable, then its copy constructor is what copies the "wrapped" type. Etc.
This is why the term "wrapper" is wrong. Thinking of the object as being a wrapper means that you think of it as potentially exposing the semantics of the underlying type though its native copy/move/destructor methods. And that requires that "wrapper" know the type internally, which requires that it erase that type.
template <class Task = SharedProxy<Callable<void()>>>class CompositTask { public: template <class... Args> void emplace(Args&&... args) { data_.emplace_back(std::forward<Args>(args)...); }
void operator()() { for (auto& t : data_) t(); }
private: std::vector<Task> data_;};
void submit_a_task(DeepProxy<Callable<void()>> task);
CompositTask<> c;c.emplace([] { puts("Lambda Expression"); });c.emplace(std::bind([](const char* x) { puts(x); }, "Bind Expression"));c.emplace(c);submit_a_task(c);
Joël Lamotte
template <std::size_t SIZE>class TrivialWrapper { public: /* Constructors */ template <class T> TrivialWrapper(T&& data) requires !std::is_same<std::remove_cv_t<std::remove_reference_t<T>>, TrivialWrapper>::value && std::is_trivial<std::remove_cv_t<std::remove_reference_t<T>>>::value && (sizeof(std::remove_cv_t<std::remove_reference_t<T>>) <= SIZE) { memcpy(data_.get(), &data, sizeof(std::remove_cv_t<std::remove_reference_t<T>>)); } TrivialWrapper() = default; TrivialWrapper(const TrivialWrapper&) = default; TrivialWrapper(TrivialWrapper&&) = default;
TrivialWrapper& operator=(const TrivialWrapper& rhs) = default; TrivialWrapper& operator=(TrivialWrapper&&) = default;
/* Meets the Wrapper requirements */ void* get() { // The address of the wrapped object is calculated with a constant offset return data_.get(); }
private: MemoryBlock<SIZE> data_;};
On Wed, Jun 21, 2017 at 08:02:35PM -0700, Mingxin Wang wrote:
> I designed another general wrapper type yesterday, which is intended for
> small and trivial types, as is shown below:
>
> template <std::size_t SIZE>
> class TrivialWrapper {
> public:
> /* Constructors */
> template <class T>
> TrivialWrapper(T&& data) requires
> !std::is_same<std::remove_cv_t<std::remove_reference_t<T>>,
> TrivialWrapper>::value &&
> std::is_trivial<std::remove_cv_t<std::remove_reference_t<T>>>::value
> &&
> (sizeof(std::remove_cv_t<std::remove_reference_t<T>>) <= SIZE) {
Why remove_cv_t here?
> memcpy(data_.get(), &data,
> sizeof(std::remove_cv_t<std::remove_reference_t<T>>));
> }
> TrivialWrapper() = default;
> TrivialWrapper(const TrivialWrapper&) = default;
> TrivialWrapper(TrivialWrapper&&) = default;
>
> TrivialWrapper& operator=(const TrivialWrapper& rhs) = default;
> TrivialWrapper& operator=(TrivialWrapper&&) = default;
This is generally broken. Consider the folloving trivial data structure:
struct c_list {
struct c_list *next, *prev;
int data;
};
It is a trivial data structure but if you start moving (or copying) it around
with your wrapper things will break badly.
c_list head;head.next = nullptr;head.prev = nullptr;head.data = 3;TrivialWrapper<sizeof(c_list)> w(head);printf("%d\n", static_cast<c_list*>(w.get())->data);
struct Demo { void operator()() { for (int i = 0; i < 10; ++i) { printf("%d\n", d[i]); } }
int d[10];};
Demo x;for (int i = 0; i < 10; ++i) { x.d[i] = i; /// Initialize x with 0~9}proxy<Callable<void()>, TrivialWrapper<sizeof(Demo)>> p1(x), p2; /// p1 and p2 are proxy typesp2 = p1;p1(); /// Print 0~9p2(); /// Print 0~9