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.
If X has to obtain the resource through the explicit object parameter, then you can control how the resource is accessed, such as requiring the mutex lock for example. Maybe define the data members in a separate structure which is included in X via some helper that, in conjunction with the explicit object parameters, controls access? It is a bit cumbersome though since anyone can still add data members to the wrong place...
ReplyDelete