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 now*
Before 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)*).
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.
<
Loading Image...>
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. Thus, it is difficult to
convert every concept into a proxy, but on the contrary, it is easy to
generate a concept from any proxy.
*How should the compiler generate the code for a proxy?*
As Mr. Bengt Gustafsson wrote in the mail:
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".
Providing we have a proxy âFooâ declared as:
proxy Foo {
void operator()();
double f();
void g(int);
};
Here is the equivalent of the code that the compiler would automatically
generate for the proxy:
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>
};
*In what way can and should predefined Concepts be reused to generate
abstract classes that forward functionality to concrete types? (possibly
not at all?)*
I insist that abstract classes shall not be generated from concepts,
because concepts can do a lot more than virtual functions, but it is
possible to generate a concept from any proxy for compile-time type safety.
As Mr. Bengt Gustafsson wrote in the mail:
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 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
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.
*Ensure proper ownership management*
As 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.
If a proxy is used asynchronously, it is required to construct the object
on the heap. We can use âAsync Concurrent Invokeâ (defined in âStructural
Support for C++ Concurrencyâ, P0642, available here
<https://groups.google.com/a/isocpp.org/forum/#!topic/std-proposals/tQb9t6Hnu6M>)
and delete the object in the callback, as is shown below:
Object* object = new Object(...); // The data to be used asynchronously
P p(*object); // Declaring a proxy
con::async_concurrent_invoke([=] { delete object; }, /* some concurrent
callers */);
*Should "Static" concepts behave like "dynamic" ones only in that they
operate at compile time? Why is a different approach preferable?*
As Mr. Bengt Gustafsson wrote in the mail:
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
std::box<Callable<void()>> myFunction; // Works essentially as
std::function
std::box<MyConcept> x;
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.
As I suppose that abstract classes shall not be generated from concepts, I
do not vote for this idea. Besides, because this feature requires code
generation at compile-time, I tend to define the âproxiesâ as a âtype of
typeâ rather than a âtemplate <typename...> typenameâ.
Mingxin Wang
--
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-proposals+***@isocpp.org.
To post to this group, send email to std-***@isocpp.org.
To view this discussion on the web visit https://groups.google.com/a/isocpp.org/d/msgid/std-proposals/f6c3b3d1-f723-41de-ac23-85defc434abe%40isocpp.org.