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();
}
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);
}
};
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.