The Proxies - A Language Feature Decoupling Implementations from Requirements of Polymorphism

229 views
Skip to first unread message

Mingxin Wang

unread,
Jun 13, 2017, 9:49:43 AM6/13/17
to ISO C++ Standard - Future Proposals
Introduction

This thread is an update version for "Adding the Keyword "proxy" in C++" and "Adding the Keyword "interface" in C++".

After a short term discussion, Mr. Bengt Gustafsson and I have come up with a better solution to decouple implementations from requirements of polymorphism. Comparing to the previous two solutions, not only does this solution can be used wherever the two solutions are suitable for, but also has more reasonable architecture and does not require new syntax.

Andrea Proli's paper about "Dynamic Generic Programming with Virtual Concepts" is a very enlightening proposal and aims to solve the same challenge as this solution does, and it has already clarified the motivation of this work. However, I think there is still some room to improve. "DGPVC" is used to refer to this paper in the following sections.

Design Goals

  • Efficiency: If this feature is properly used, the performance shall not be lower than a hand-wrote implementation without this feature for a same requirement.
  • Support for different lifetime management strategies: including but not limited to reference semantics, value semantics, shared semantics, COW (Copy On Write)、SOO (Small Object Optimization, aka SBO, Small Buffer Optimization) and other known advanced GC algorithms.
  • Ease of use: a type-erased type shall behave as the concrete type.
  • Not violate the rule of the type system: a type-erased type shall be a standard-layout class.

Design Decisions

  • The type requirements shall be specified by a pure-virtual class.
In my earlier post, some feedback suggests that it will be elegant to specify type requirements with the Concepts TS. DGPVC is a solution that adopts this. However, on the one hand, it introduces new syntax, mixing the "concepts" with the "virtual qualifier", which makes the types ambiguous. From the code snippets included in the paper, we can tell that "virtual concept" is an "auto-generated" type. Comparing to introducing new syntax, I prefer to make it a "magic class template", which at least "looks like a type" and much easier to understand. On the other hand, I haven't seen much about how to implement the entire solution introduced in DGPVC, and it remains hard for me to imagine how are we supposed to implement for the expressions that cannot be declared virtual, e.g. friend functions that take values of the concrete type as parameters.
  • The lifetime management strategies shall be specified by a type having the "Wrapper" semantics.
The "Wrapper" semantics is associated with the responsibility for addressing, and may have different lifetime management strategies. It seems difficult to extend DGPVC with other lifetime management strategies as it only support the basic "reference semantics" and "value semantics", e.g. reference-counting based algorithm and more complicated GC algorithms. In this solution, users are free to specify different types of wrapper for any lifetime management requirements. Besides, I think it is rude to couple the "characteristics for construction and destruction" with other expressions required in DGPVC. When it is not required to manage the lifetime issue (e.g.,with reference semantics), the constraints related to constructors and destructors are redundant; conversely, when we need value semantics, it is natural that the type being type-eraing shall be at least MoveConstructible most of the time. This problem does not exist in my solution as constructors and destructors are not able to declared pure virtual, and a wrapper type may carry such constraints if necessary.
  • The type-erased type shall be a specification of a class template, which is duck-typed by the compiler.
  • When calling a specific member function of a type-erased type, the parameters shall be forwarded to a "conversion table".
Unlike std::function<R(Args...)> that provide a standard version of "R operator()(Args...)", I think it is much general to provide the routine that forward any parameter to the standard conversion table, e.g., providing "template <class... _Args> R operator()(_Args&&...)" instead of "R operator()(Args...)". This will not have effects on the function itself, and it is friendly for the "standard" conversion table.

Technical Specifications

Wrapper requirements

A 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 proxy

Expression "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 Wrappers

Class "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.

Code Generation

Take the "Callable" interface as an example,

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;
};

The code that the compiler will possibly generate for the type "proxy<Callable<R(Args...)>, W>" is as shown below:

#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_;
};

Examples

The following code is well-formed with the class template "Callable" defined above:

#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;
}

I am looking forward to your comments and suggestions!

Thank you!

Mingxin Wang
src.zip

Nicol Bolas

unread,
Jun 13, 2017, 11:20:26 AM6/13/17
to ISO C++ Standard - Future Proposals
On Tuesday, June 13, 2017 at 9:49:43 AM UTC-4, Mingxin Wang wrote:
Technical Specifications

Wrapper requirements

A 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 proxy

Expression "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.

Prototypes for Wrappers

Class "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.

This is one of the problems of going to a generic mechanism for handling semantics: the rabbit hole of possibilities is infinite.

That being said, since the "wrapper" is part of the proxy's template declaration, it is possible to have the "wrapper" actually influence the proxy's interface. So the "wrapper" could affect the definition of the proxy's template constructor from `T`, as well as the template function which extracts a pointer/reference to the proxied type. A `SharedWrapper` could therefore allow you to pass a `shared_ptr<T>`, as well as allow you to extract a `shared_ptr<T>`.

Mingxin Wang

unread,
Jun 14, 2017, 4:11:55 AM6/14/17
to ISO C++ Standard - Future Proposals
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 Specifications

Wrapper requirements

A 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 proxy

Expression "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.

Yes, This is a question worth considering. Luckily, this does not require reconstructing, we can simply "add" the feature to this solution. Maybe we can have something like "std::any_cast":

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;

Implementations may use dynamic_cast for RTTI. To clarify this issue, we may as well assume there is an invisible "helper" member function in the class template proxy for the function templates defined above, as is shown below:

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 Wrappers

Class "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.

I am sorry about the misspelling. It should be "DeferredWrapper" rather than "DefferedWrapper". Still, I suggest we need more feedback and discuss the naming issue later.
 
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.

Like class template std::shared_ptr, "SharedWrapper" is also based on reference-counting.The differences between the two are:
  • std::shared_ptr is based on pointer semantics, whose general layout requires two unrelated memory block (that is why we prefer to use "std::make_shared" to avoid "twice memory allocation", and why some people dislike std::shared_ptr), while SharedWrapper only requires one memory block maintaining both the reference-count and the value of the concrete object (rather than a pointer of the concrete object). Thus there is no conversion between std::shared_ptr and SharedWrapper as they have different structures.
  • std::shared_ptr is type-specific, while SharedWrapper is type-erased.
See the implementation (shared_wrapper.hpp) included in src.zip for more details.

Nicol Bolas

unread,
Jun 14, 2017, 1:13:47 PM6/14/17
to ISO C++ Standard - Future Proposals
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 Specifications

Wrapper requirements

A 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.

If we redefine this on a conceptual level, then it becomes clear how we can solve the double-erasure problem. I'll rename the type, so it's clear when I'm talking about one vs. the other.

A "semantic" is an object which provides storage and semantics for a value. However, it doesn't know what the type of that value is; that is maintained by the user of the class.

As such, the compiler-generated proxy would generate functions in its abstract base class based on the interface the "semantic" provides. The `T`-specific derived classes would implement versions that call the "semantic"'s interface functions, specifying the `T` that they work with. This means those interface functions in the "semantic" must be template functions. Note that in most cases, the "semantic" interfaces don't need to be provided with the `T` object itself; just the `T` that is being utilized.

The available "semantic" interfaces would be:

* `bind`: Stores a given `T`. Required, though it can refuse to accept `T`s that don't fit some requirement. The proxy will forward such requirements.
* `get`: Given the type `T` to retrieve, retrieves a pointer to that type. This is required.
* `copy`: Copies the `T` from one "semantic" into an unbound "semantic". If this is deleted, then the proxy is non-copyable. If this is not present, then the proxy will just copy the "semantic" object directly.
* `move`: Moves the `T` from one "semantic" into another. If this is deleted, then the proxy is non-moveable. If this is not present, then the proxy will just move the "semantic" object directly.
* `unbind`: Unbinds the `T`, potentially calling its destructor. If this function is not present, then the proxy will assume calling the destructor is enough.

The proxy should forward any `noexcept` guarantees of these functions. The proxy will also generate the code that is needed to handle assignment (as `unbind` followed by `copy`/`move`). Though this does lead to the `variant` question of what happens if a copy/move assignment fails.

A trivial "semantic" only implements `bind` and `get` (the latter only being needed for the `any_cast` equivalent), allowing the compiler-generated copy/move constructor/assignments to work. Implementing `copy|move` requires also implementing `unbind`, and implementing `unbind` requires implementing `copy|move`.

This puts all of the type-erasure machinery in one place: the proxy. This means that "ValueSemantic" doesn't need any storage beyond the SSO arena.

Klaim - Joël Lamotte

unread,
Jun 16, 2017, 12:16:24 PM6/16/17
to std-pr...@isocpp.org

On 13 June 2017 at 15:49, Mingxin Wang <wmx16...@163.com> wrote:
Class template proxy

Expression "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.


Could you clarify: if a system want to take anything that have the interface described by I, how should it 

    some_system.take_it_and_do_something( ??? object );

I believe this would be really too restrictive:

    some_system.take_it_and_do_something( proxy<I,W> object );

Because it would force the user code to use only one ownership strategy for anything passed to this function.
In my experience, I want the system to ignore if it will own or not the object, let the user decide, as long as
the proxy is usable as a normal object.
In my opinion, the fact that the real object is shared or not should not be imposed by the system consuming the proxy.

Joël Lamotte

Mingxin Wang

unread,
Jun 16, 2017, 9:07:13 PM6/16/17
to ISO C++ Standard - Future Proposals
On Thursday, June 15, 2017 at 1:13:47 AM UTC+8, Nicol Bolas wrote:
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 Specifications

Wrapper requirements

A 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?

Actually, a type that meets the Wrapper requirements not only can be constructible from any type (e.g. DeferredWrapper), but also can even not to be constructible from any type at all (so that proxy<I, W> is not constructible from any type; this case is meaningless but legal).There shall be constraints on constructor of a proxy, e.g. requires(T&&) { { W(std::forward<T>(t)) }; }. So there are no particular requirements for the constructor of a wrapper, e.g. T shall be CopyConstructible for DeepWrapper, and DeepWrapper(std::packaged_task<void()>{...}) is ill-formed (because std::packaged_task is not CopyConstructible). 

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"?

The requirements for polymorphism and addressing are fully decoupled from each other, so that users are free to use the proxy with any combination of any pure virtual class and wrappers.

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.

I am sorry that "DeepWrapper" should be "DeepProxy" (which is defined in src.zip/proxy.hpp, DeepProxy<I, N> == proxy<I, DeepWrapper<N>>). The performance test is actually between DeepProxy<Callable<void()>, 16u> and std::function<void()>.
 
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).

As I mentioned before, the size difference between the two objects is "one-pointer size", which is, on my platform, 8 bytes. Note that DeepWrapper is not the only implementation for the Wrapper requirements, there are two other implementations included in src.zip (DeferredWrapper and SharedWrapper). The reason why I only tested DeepProxy<Callable<void()>, 16u> and std::function<void()> is that there are little utilities we have in the standard that have similar functions as the proxy does.

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.

A wrapper may support reference semantics (e.g. DeferredWrapper). The implementation for the copy/move semantics for a wrapper may not necessarily to be polymorphic (except for "DeepWrapper", the other two implementations included in src.zip are not polymorphic at all). After all, where there is polymorphism, there is overhead.

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.

Take the 3 implementations of the wrapper included in src.zip as an example, the necessity of the copy/move/destructor to be polymorphic is as shown below:

Mingxin Wang

unread,
Jun 16, 2017, 9:55:15 PM6/16/17
to ISO C++ Standard - Future Proposals
Any type that has the required expressions specified by I and can be used to construct the wrapper specified by W, can be implicitly convertible to proxy<I, W>. It is true that users are responsible to specify ownership strategies in type declarations, but this only happens in a certain context. When there is no need for polymorphism, I prefer to declare a function as a template. Here is an example use case for proxies and templates:

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_;
};

Proxy usually works well with templates. The code above is a class template with a default type, which satisfies the requirements specified by Callable<void()>. Providing there is another function declared as follows:

void submit_a_task(DeepProxy<Callable<void()>> task);

The following code is well-formed for client code:

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);

Every conversion is done implicitly.
 

Joël Lamotte


Mingxin Wang

Mingxin Wang

unread,
Jun 21, 2017, 11:02:35 PM6/21/17
to ISO C++ Standard - Future Proposals
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) {
    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_;
};


I am wondering if these design for wrappers (DeferredWrapper, DeepWrapper, SharedWrapper and TrivialWrapper) are adequate to be added to the standard (discard of naming).

Magnus Fromreide

unread,
Jun 22, 2017, 4:24:51 AM6/22/17
to std-pr...@isocpp.org
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.

> /* 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_;
> };
>
>
> I am wondering if these design for wrappers (DeferredWrapper, DeepWrapper,
> SharedWrapper and TrivialWrapper) are adequate to be added to the standard
> (discard of naming).

I think you need to put in a lot of more work on why they are generally
useful.

/MF

Mingxin Wang

unread,
Jun 22, 2017, 5:24:48 AM6/22/17
to ISO C++ Standard - Future Proposals
On Thursday, June 22, 2017 at 4:24:51 PM UTC+8, Magnus Fromreide wrote:
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?

Because T may have const or volatile qualifiers, "std::remove_cv_t<std::remove_reference_t<T>>" represents the raw type of T.
 
>     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.

Actually, I did not see the problem as the following client code is well-formed:

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);

The code above is not elegent as there is a type cast. However, wrapper types are not directly used in this solution, they are designed for the proxies. Suppose there is a trivial callable type defined as below:

struct Demo {
  void operator()() {
    for (int i = 0; i < 10; ++i) {
      printf("%d\n", d[i]);
    }
  }

  int d[10];
};

The following cilent code is well-formed:

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 types
p2 = p1;
p1(); /// Print 0~9
p2(); /// Print 0~9

When assign p1 to p2, the memory block of p1 is directly copied to p2. Then, p1 and p2 holds the same value.

Everything a TrivialWrapper can solve can be solved with DeepWrapper. The differences between the two are the performance and scope of application:
  • TrivialWrapper usually has higher runtime performance, because it is not polymorphic, and
  • DeepWrapper usually has a wider scope of application, because it can be converted from a type of any size and regardless of whether the type is trivial or not.
I prefer to use TrivialWrapper in performance sensitive cases, and DeepWrapper is recommended to be used in generality sensitive cases.
Reply all
Reply to author
Forward
0 new messages