Date: Fri, 3 Dec 2021 01:23:14 +0100
Hi,
Being able to create constexpr binding of non-trivial types seems useful
for reflection and metaprogramming. For example, for querying and storing a
name that is intended to be used during the runtime. This can be done (as
in the TS) with const char*, but 1/ this is not very ergonomic and 2/ does
this imply that every data resulting from a reflection query will be stored
in the binary of the TU, regardless of whether or not it's needed? (I think
it does). More generally, users might want to store dynamic arrays or
arbitrary structure in a constexpr binding. For dynamic arrays, there is
the ugly but fairly straightforward workaround of converting them to
std::array by performing the computation twice, first for getting the size,
secondly for loading the data into the std::array - ugly but doable, but
we're still limited to storing trivial types. For arbitrary structures this
can get quite difficult : for example how do I convert my AST structure as
a bunch of arrays of trivial types?
The current proposal (P1974) to resolve this is to introduce a new type
qualifier that introduces transitive constness.
I think it would be nice to explore other solutions : deep-constness in the
type system is great, however (i suspect) it will be a lot of work to
specify, and the resulting ergonomy, if I understand the proposal
correctly, is not ideal - the proposal consists in introducing transitive
constness only for a single branch of a type expression, thus the type for
the name of the reflection API would be something like
std::basic_string<propconst char>. Something that can be dealt with with
aliases, but I think it would be largely preferable to not introduce such a
schism between the types that can be used in a constexpr binding and types
that can't.
I would like to propose a rough draft for another solution :
Instead of extending the type system, introduce an attribute that certifies
deep-constness (let's call it [[deep_const]] or [[pure]]). A non-trivial
class can only be used as the type of a constexpr binding if it's annotated
with that attribute.
Any class C annotated with that attribute must satisfy these requirements :
1/ No public data members whose types are not deep-const
2/ All data members of pointers or reference type must have a deep-const
form
3/ Every member function (and friend functions, and the - possibly trivial
- special member functions) taking a "const& C" or "const&& C" must not
dereference, pass to another function, or return, a pointer or reference
subobject of C which is not in its deep-const form
4/ No mutable data member
(To make this more user friendly, I think the third constraint should not
be more of an opt-in behavior,
where the compiler automatically insert the necessary implicit cast to
fulfill it)
A type is deep-const if it is :
* A const trivial type
* A const pointer, whose pointee is a deep-const type
* A const reference, whose referee is a deep-const type
* A const class/struct annotated with deep-const
The deep-const form of a type is defined by :
* DeepConst(T) = const T, defined if T is a deep-const type
* DeepConst(T&) = const T&, defined if T is a deep-const type
* DeepConst(T*) = const * DeepConst(T)
This essentially amounts to just lifting `deep-constness` out of the type
system and making it a trait.
The pros are :
* No extension to the type system, arguably easier to use and understand
than propconst
* no special types or qualifiers for constexpr bindings
* the most common containers in the standard library fullfill these
requirements already
The cons :
* ?
Curious to hear what you think and listen to other ideas in that space.
cheers,
-JB
Being able to create constexpr binding of non-trivial types seems useful
for reflection and metaprogramming. For example, for querying and storing a
name that is intended to be used during the runtime. This can be done (as
in the TS) with const char*, but 1/ this is not very ergonomic and 2/ does
this imply that every data resulting from a reflection query will be stored
in the binary of the TU, regardless of whether or not it's needed? (I think
it does). More generally, users might want to store dynamic arrays or
arbitrary structure in a constexpr binding. For dynamic arrays, there is
the ugly but fairly straightforward workaround of converting them to
std::array by performing the computation twice, first for getting the size,
secondly for loading the data into the std::array - ugly but doable, but
we're still limited to storing trivial types. For arbitrary structures this
can get quite difficult : for example how do I convert my AST structure as
a bunch of arrays of trivial types?
The current proposal (P1974) to resolve this is to introduce a new type
qualifier that introduces transitive constness.
I think it would be nice to explore other solutions : deep-constness in the
type system is great, however (i suspect) it will be a lot of work to
specify, and the resulting ergonomy, if I understand the proposal
correctly, is not ideal - the proposal consists in introducing transitive
constness only for a single branch of a type expression, thus the type for
the name of the reflection API would be something like
std::basic_string<propconst char>. Something that can be dealt with with
aliases, but I think it would be largely preferable to not introduce such a
schism between the types that can be used in a constexpr binding and types
that can't.
I would like to propose a rough draft for another solution :
Instead of extending the type system, introduce an attribute that certifies
deep-constness (let's call it [[deep_const]] or [[pure]]). A non-trivial
class can only be used as the type of a constexpr binding if it's annotated
with that attribute.
Any class C annotated with that attribute must satisfy these requirements :
1/ No public data members whose types are not deep-const
2/ All data members of pointers or reference type must have a deep-const
form
3/ Every member function (and friend functions, and the - possibly trivial
- special member functions) taking a "const& C" or "const&& C" must not
dereference, pass to another function, or return, a pointer or reference
subobject of C which is not in its deep-const form
4/ No mutable data member
(To make this more user friendly, I think the third constraint should not
be more of an opt-in behavior,
where the compiler automatically insert the necessary implicit cast to
fulfill it)
A type is deep-const if it is :
* A const trivial type
* A const pointer, whose pointee is a deep-const type
* A const reference, whose referee is a deep-const type
* A const class/struct annotated with deep-const
The deep-const form of a type is defined by :
* DeepConst(T) = const T, defined if T is a deep-const type
* DeepConst(T&) = const T&, defined if T is a deep-const type
* DeepConst(T*) = const * DeepConst(T)
This essentially amounts to just lifting `deep-constness` out of the type
system and making it a trait.
The pros are :
* No extension to the type system, arguably easier to use and understand
than propconst
* no special types or qualifiers for constexpr bindings
* the most common containers in the standard library fullfill these
requirements already
The cons :
* ?
Curious to hear what you think and listen to other ideas in that space.
cheers,
-JB
Received on 2021-12-02 18:23:27