Friday, August 18, 2023

User-defined class qualifiers in C++23

It is generally known that type qualifiers (such as const and volatile in C++) can be regarded as a form of subtyping: for instance, const T is a supertype of T because the interface (available operations) of T are strictly wider than those of const T. Foster et al. call a qualifier q positive if q T is a supertype of T, and negative it if is the other way around. Without real loss of generality, in what follows we only consider negative qualifiers, where q T is a subtype of (extends the interface of) T.

C++23 explicit object parameters (coloquially known as "deducing this") allow for a particularly concise and effective realization of user-defined qualifiers for class types beyond what the language provides natively. For instance, this is a syntactically complete implementation of qualifier mut, the dual/inverse of const (not to be confused with mutable):

template<typename T>
struct mut: T
{
using T::T;
};

template<typename T>
T& as_const(T& x) { return x;}

template<typename T>
T& as_const(mut<T>& x) { return x;}

struct X
{
void foo() {}
void bar(this mut<X>&) {}
};

int main()
{
mut<X> x;
x.foo();
x.bar();

auto& y = as_const(x);
y.foo();
y.bar(); // error: cannot convert argument 1 from 'X' to 'mut<X> &'

X& z = x;
z.foo();
z.bar(); // error: cannot convert argument 1 from 'X' to 'mut<X> &'
}

The class X has a regular (generally accessible) member function foo and then bar, which is only accessible to instances of the form mut<X>. Access checking and implicit and explicit conversion between subtype mut<X> and mut<X> work as expected.

With some help fom Boost.Mp11, the idiom can be generalized to the case of several qualifiers:

#include <boost/mp11/algorithm.hpp>
#include <boost/mp11/list.hpp>
#include <type_traits>

template<typename T,typename... Qualifiers>
struct access: T
{
using qualifier_list=boost::mp11::mp_list<Qualifiers...>;

using T::T;
};

template<typename T, typename... Qualifiers>
concept qualified =
(boost::mp11::mp_contains<
typename std::remove_cvref_t<T>::qualifier_list,
Qualifiers>::value && ...);

// some qualifiers
struct mut;
struct synchronized;

template<typename T>
concept is_mut = qualified<T, mut>;

template<typename T>
concept is_synchronized = qualified<T, synchronized>;

struct X
{
void foo() {}

template<is_mut Self>
void bar(this Self&&) {}

template<is_synchronized Self>
void baz(this Self&&) {}

template<typename Self>
void qux(this Self&&)
requires qualified<Self, mut, synchronized>
{}
};

int main()
{
access<X, mut> x;

x.foo();
x.bar();
x.baz(); // error: associated constraints are not satisfied
x.qux(); // error: associated constraints are not satisfied

X y;
x.foo();
y.bar(); // error: associated constraints are not satisfied

access<X, mut, synchronized> z;
z.bar();
z.baz();
z.qux();
}
One difficulty remains, though:
int main()
{
access<X, mut, synchronized> z;
//...
access<X, mut>& w=z; // error: cannot convert from
// 'access<X,mut,synchronized>'
// to 'access<X,mut> &'

}
access<T,Qualifiers...>& converts to T&, but not to access<T,Qualifiers2...>&  where Qualifiers2 is a subset of  Qualifiers (for the mathematically inclined, qualifiers q1, ... , qN over a type T induce a lattice of subtypes Q T, Q ⊆ {q1, ... , qN}, ordered by qualifier inclusion). Incurring undefined behavior, we could do the following:
template<typename T,typename... Qualifiers>
struct access: T
{
using qualifier_list=boost::mp11::mp_list<Qualifiers...>;

using T::T;

template<typename... Qualifiers2>
operator access<T, Qualifiers2...>&()
requires qualified<access, Qualifiers2...>
{
return reinterpret_cast<access<T, Qualifiers2...>&>(*this);
}
};
A more interesting challenge is the following: As laid out, this technique implements syntactic qualifier subtyping, but does not do anything towards enforcing the semantics associated to each qualifier: for instance, synchronized should lock a mutex automatically, and a qualifier associated to some particular invariant should assert it after each invocation to a qualifier-constraied member function. I don't know if this functionality can be more or less easily integrated into the presented framework: feedback on the matter is much welcome.