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