Q: detecting class deriving from template (w/ types & NTTPs) including std types
This is my first post here in the hope that somebody knowledgeable can-do-expert may be able to advise.
And also: please forgive me because I am about to sin ... (ie. 'thou shall not derive from standard types')
A bit of pre-context: We are re-designing/implementing/improving a new middle-ware to aid us and other accelerator-domain experts at FAIR in their beam-based feedback and signal processing applications. As one of its core functionalities, it implements a domain-object driven serialisation scheme that heavily relies on C++20 concepts, refl-cpp as pre-cursor to compile-time/static reflection, and mp-units to support type- and notable physical-unit safety in our code/algorithms and to enhance the overall end-user development experience. Both of the latter libraries/functionalities are targeted to become part of the C++ standard.
At its core, the user defines domain-objects (e.g. measurement data object/structs or control data) that contain plain and annotated C++ data types -- notably without having to rely on an IDL description -- that are passed, shared and combined within a distributed micro-service topology (N.B. 'distributed' because these devices are physically distributed across and very close to our accelerators). As an example:
struct DomainObject {
si::speed<metre_per_second> speedValue = valUnit;
double doubleValue = 42.0;
std::array<double, 3> doubleArray = { 1., 2., 3.};
Annotate<float, speed<metre_per_second>, "custom description"> annotatedSpeed = quantity_cast<si::speed<si::metre_per_second, float>>(valUnit);
Annotate<float, length<metre>, "this length is ..."> annotatedLength;
Annotate<double, si::time<second>, "time as measured ... "> annotatedTime;
Annotate<short, energy<electronvolt>, "energy of ..."> annotatedEnergy;
Annotate<std::string, NoUnit, "string documentation"> annotatedString = "Hello World!";
Annotate<CustomUserStruct, NoUnit, "user-defined object1"> customStruct = CustomUserStruct{1.1, 2.2, 3.3};
Annotate<std::array<int, 3>, NoUnit, "user-defined array"> annotatedArray = { 10, 20, 30};
DomainObject() : annotatedSpeed(10.0_q_km_per_h), annotatedLength(100.0_q_m), annotatedTime(9.8_q_s){};
};
A more complete mock-up example can be explored using the compiler-explorer.
While the serialisation and compile-time reflections work without, we wrote a transparent Annotated<..>
wrapper interfaces
// Annotate using mp-unit's quantity and operator inheritance, unit- and type-safety checks
template <units::Representation Rep, units::Quantity Q, const units::basic_fixed_string description>
struct Annotate<Rep, Q, description> : public units::quantity<typename Q::dimension, typename Q::unit, Rep> {
// [..]
// needed for API description/compile-time serialisation
[[nodiscard]] constexpr const String getUnit() const noexcept { return String(units::detail::unit_text<dimension, unit>().ascii().c_str()); }
[[nodiscard]] constexpr const String getDescription() const noexcept { return description.c_str(); }
[[nodiscard]] constexpr const String getTypeName() const noexcept { return typeName<Rep>(); }
//[..]
};
and
// Annotate using mp-unit's quantity/unit info (only) w/o unit- and type-safety checks
template <ClassType T, typename Q, const units::basic_fixed_string description>
struct Annotate<T, Q, description> : T { // inherit from T directly to inherit also all its potential operators & member functions
// [..]
};
as part of this scheme that allows attaching additional (but optional) meta-information to instruct the compile-time serialiser and other general behaviours, for example: whether the data is read-only (w.r.t. remote-IO but still modifiable internally); whether it is optional/individually settable, or needs to be set within a given set of other fields, etc.). For the server-side processing, this does not provide any relevant (hidden) state and the wrapper is thus designed to be as transparent as possible and to -- importantly -- pass-through any operator or member functions of the container or user-supplied struct.
While this works fine for polymorphic types (notably those used by the mp-units library, first template specialisation), I am having some small troubles with deriving from std containers. While this scheme works fine for the serialisation and general processing, there is a noticable difference w.r.t. detecting the base template the Annotate<>
specialisation is derived from. In the example code this is demonstrated at the end:
std::cout << std::boolalpha;
std::cout << "speedValue is an array or vector: " << is_array_or_vector<decltype(data.speedValue)> << std::endl; // OK
std::cout << "doubleArray is an array or vector: " << is_array_or_vector<decltype(data.doubleArray)> << std::endl; // OK
std::cout << "annotatedArray is an array or vector: " << is_array_or_vector<decltype(data.annotatedArray)> << std::endl; // not OK
std::cout << "annotatedArray is an array or vector: " << is_array_or_vector<decltype(data.annotatedArray.rawValue())> << std::endl; // alt1: not OK
std::cout << "annotatedArray is an array or vector: " << units::is_derived_from_specialization_of<decltype(data.annotatedArray), std::vector> << std::endl; // alt2: not OK
// std::cout << "val8 is an array or vector: " << std::is_base_of<std::array, decltype(data.val8)>::value << std::endl; // not work because being a template & with NTTPs :-|
data.annotatedArray[2] = 42; // OK -- works as designed
std::cout << "annotatedArray content: " << data.annotatedArray << std::endl; // OK -- works as designed
std::cout << "doubleArray content: " << data.doubleArray << std::endl; // OK -- works as designed
//std::cout << "annotatedArray content: " << data.annotatedArray << std::endl; // not OK
std::cout << "annotatedArray content: " << data.annotatedArray.rawValue() << std::endl; // work-around OK but ugly from a users perspective
While the basic functionalities do work -- on the meta-programming level I am having trouble to differentiate or better to not differentiate between, e.g. an 'std::array' and something that derives from it. Part of the issue seems to be that most trait functions rely on template<typename... Params, template<typename...> typename Type>
of some sorts but the templates I am having trouble with also contains NTTPs (ie. something that is not caught by typename ...
)
I hope the practical solution to this is obvious to someone or an hardcore C++20 fan. Any constructive help would be much appreciated
The full example, also at compiler-explorer:
// ############# library code mock-up ################ -- user code see below
#include <units/concepts.h>
#include <units/quantity.h>
#include <units/quantity_io.h>
#include <iostream>
#include <string>
// extract of concept definitions
template<typename T>
inline constexpr bool is_array_or_vector = false;
template<typename T, typename A>
inline constexpr bool is_array_or_vector<std::vector<T, A>> = true;
template<typename T, typename A>
inline constexpr bool is_array_or_vector<const std::vector<T, A>> = true;
template<typename T, std::size_t N>
inline constexpr bool is_array_or_vector<std::array<T, N>> = true;
template<typename T, std::size_t N>
inline constexpr bool is_array_or_vector<const std::array<T, N>> = true;
template<typename T>
concept ArrayOrVector = is_array_or_vector<T>;
// N.B. extend this for custom classes using type-traits to query nicer class-type name
template <typename T, typename Tp = typename std::remove_const<T>::type>
requires (!is_array_or_vector<Tp>)
constexpr const char* typeName() noexcept {
if (std::is_same<Tp, std::byte>::value) { return "byte"; }
if (std::is_same<Tp, char>::value) { return "char"; }
if (std::is_same<Tp, short>::value) { return "short"; }
if (std::is_same<Tp, int>::value) { return "int"; }
if (std::is_same<Tp, long>::value) { return "long"; }
if (std::is_same<Tp, float>::value) { return "float"; }
if (std::is_same<Tp, double>::value) { return "double"; }
if (units::is_derived_from_specialization_of<Tp, std::basic_string>) { return "string"; }
return typeid(T).name();
}
template<ArrayOrVector T>
constexpr const char* typeName() noexcept {
return "it's an std::array or std::vector";
}
template <typename T>
concept ClassType = std::is_class<T>::value;
using NoUnit = units::dimensionless<units::one>;
template <typename Rep, typename Q = units::dimensionless<units::one>, const units::basic_fixed_string description = "" /*, more user-specific NTTPs */>
struct Annotate; // prototype template -- N.B. there are two implementations since most non-numeric classes do not qualify as units::Representation
// Annotate using mp-unit's quantity and operator inheritance, unit- and type-safety checks
template <units::Representation Rep, units::Quantity Q, const units::basic_fixed_string description>
struct Annotate<Rep, Q, description> : public units::quantity<typename Q::dimension, typename Q::unit, Rep> {
using dimension = typename Q::dimension;
using unit = typename Q::unit;
using rep = Rep;
using R = units::quantity<dimension, unit, Rep>;
using String = std::string_view;
constexpr Annotate() : R(){};
constexpr explicit(!std::is_trivial_v<Rep>) Annotate(const R& t) : R(t) {}
constexpr explicit(!std::is_trivial_v<Rep>) Annotate(R&& t) : R(std::move(t)) {}
constexpr Annotate& operator=(const R& t) { R::operator=(t); return *this; }
constexpr Annotate& operator=(R&& t) { R::operator=(std::move(t)); return *this; }
// needed for API description/compile-time serialisation
[[nodiscard]] constexpr const String getUnit() const noexcept { return String(units::detail::unit_text<dimension, unit>().ascii().c_str()); }
[[nodiscard]] constexpr const String getDescription() const noexcept { return description.c_str(); }
[[nodiscard]] constexpr const String getTypeName() const noexcept { return typeName<Rep>(); }
// [..]
// raw data access
[[nodiscard]] constexpr inline rep& rawValue() & noexcept { return this->number(); }
[[nodiscard]] constexpr inline const rep& rawValue() const & noexcept { return this->number(); }
[[nodiscard]] constexpr inline rep&& rawValue() && noexcept { return std::move(this->number()); }
[[nodiscard]] constexpr inline const rep&& rawValue() const && noexcept { return std::move(this->number()); }
};
// Annotate using mp-unit's quantity/unit info (only) w/o unit- and type-safety checks
template <ClassType T, typename Q, const units::basic_fixed_string description>
struct Annotate<T, Q, description> : T { // inherit from T directly to inherit also all its potential operators & member functions
using dimension = typename Q::dimension;
using unit = typename Q::unit;
using rep = T;
using String = std::string_view;
constexpr Annotate() : T() {}
explicit(!std::is_trivial_v<T>) constexpr Annotate(const T& t) : T(t) {}
explicit(!std::is_trivial_v<T>) constexpr Annotate(T&& t) : T(std::move(t)) {}
template <class... S>
Annotate(S&&... v) : T{std::forward<S>(v)...} {}
template <class S, std::enable_if_t<std::is_constructible<T, std::initializer_list<S>>::value, int> = 0>
Annotate(std::initializer_list<S> init) : T(init) {}
template <typename U>
requires(std::is_assignable<T, U>::value) Annotate(const U& u) : T(u) {}
template <typename U>
requires(std::is_assignable<T, U>::value) Annotate(U&& u) : T(std::move(u)) {}
// needed for API description/compile-time serialisation
[[nodiscard]] constexpr const String getUnit() const noexcept { return String(units::detail::unit_text<dimension, unit>().ascii().c_str()); }
[[nodiscard]] constexpr const String getDescription() const noexcept { return description.c_str(); }
[[nodiscard]] constexpr const String getTypeName() const noexcept { return typeName<T>(); }
// [..]
// raw data access
[[nodiscard]] constexpr inline T& rawValue() & noexcept { return *this; }
[[nodiscard]] constexpr inline const T& rawValue() const & noexcept { return *this; }
[[nodiscard]] constexpr inline T&& rawValue() && noexcept { return std::move(*this); }
[[nodiscard]] constexpr inline const T&& rawValue() const && noexcept { return std::move(*this); }
};
// nicer print-out for arrays and vectors
template<ArrayOrVector T>
std::ostream &operator<<(std::ostream &os, const T &v) {
using ValueType = typename T::value_type;
os << '{';
std::copy(std::begin(v), std::end(v), std::ostream_iterator<ValueType>(os,", "));
os << "}";
return os;
}
int counter = 0; // just for debugging
template <typename T>
constexpr void printMetaInfo(const T& value) {
if constexpr (requires { value.getTypeName(); }) { // simplistic 'Annotate' detection
std::cout << "AN-value" << counter++ << ": '" << value.rawValue() << "' type: '" << value.getTypeName() << "' unit: '" << value.getUnit() << "' description: '" << value.getDescription() << "'\n";
} else { // not annotated variable
std::cout << "NA-value" << counter++ << ": '" << value << "' type: '" << typeName<decltype(value)>() << "'\n";
}
}
// ########################################
// ############# user code ################
// ########################################
#include <units/isq/si/length.h>
#include <units/isq/si/speed.h>
#include <units/isq/si/energy.h>
struct CustomUserStruct {
double x = 1;
double y = 2;
double z = 3;
};
// N.B. 'operator<<' replaced by generic compile-time reflection
std::ostream& operator<<(std::ostream& os, const CustomUserStruct& m) noexcept {
return os << "{x:" << m.x << ", y: " << m.y << ", z: " << m.z << '}';
}
using namespace units::isq;
using namespace units::isq::si;
using namespace std::literals;
constexpr Speed auto valUnit = 110._q_km_per_h;
struct DomainObject {
si::speed<metre_per_second> speedValue = valUnit;
double doubleValue = 42.0;
std::array<double, 3> doubleArray = { 1., 2., 3.};
Annotate<float, speed<metre_per_second>, "custom description"> annotatedSpeed = quantity_cast<si::speed<si::metre_per_second, float>>(valUnit);
Annotate<float, length<metre>, "this length is ..."> annotatedLength;
Annotate<double, si::time<second>, "time as measured ... "> annotatedTime;
Annotate<short, energy<electronvolt>, "energy of ..."> annotatedEnergy;
Annotate<std::string, NoUnit, "string documentation"> annotatedString = "Hello World!";
Annotate<CustomUserStruct, NoUnit, "user-defined object1"> customStruct = CustomUserStruct{1.1, 2.2, 3.3};
Annotate<std::array<int, 3>, NoUnit, "user-defined array"> annotatedArray = { 10, 20, 30};
DomainObject() : annotatedSpeed(10.0_q_km_per_h), annotatedLength(100.0_q_m), annotatedTime(9.8_q_s){};
};
constexpr Speed auto avg_speed(Length auto d, Time auto t) { return d / t; }
int main() {
DomainObject data;
// N.B. loop replaced with compile-time reflection
printMetaInfo(data.speedValue);
printMetaInfo(data.doubleValue);
printMetaInfo(data.doubleArray);
printMetaInfo(data.annotatedSpeed);
printMetaInfo(data.annotatedLength);
printMetaInfo(data.annotatedTime);
printMetaInfo(data.annotatedEnergy);
printMetaInfo(data.annotatedString);
printMetaInfo(data.customStruct);
printMetaInfo(data.annotatedArray);
Speed auto fastRunnerVelocity = avg_speed(data.annotatedLength, data.annotatedTime);
std::cout << "a fast athlete can run: " << fastRunnerVelocity << "\n\n";
Speed auto ok1 = fastRunnerVelocity + 10_q_m_per_s;
Speed auto ok2 = fastRunnerVelocity*0.5;
//Speed auto error = fastRunnerVelocity + 10; // causes (correct) compile-time error for missing/mismatched unit
data.annotatedSpeed.number() = data.annotatedSpeed.number() + 20; // N.B. raw-access needed for raw serialiser read/write access - OK
std::cout << std::boolalpha;
std::cout << "speedValue is an array or vector: " << is_array_or_vector<decltype(data.speedValue)> << std::endl; // OK
std::cout << "doubleArray is an array or vector: " << is_array_or_vector<decltype(data.doubleArray)> << std::endl; // OK
std::cout << "annotatedArray is an array or vector: " << is_array_or_vector<decltype(data.annotatedArray)> << std::endl; // not OK
std::cout << "annotatedArray is an array or vector: " << is_array_or_vector<decltype(data.annotatedArray.rawValue())> << std::endl; // alt1: not OK
std::cout << "annotatedArray is an array or vector: " << units::is_derived_from_specialization_of<decltype(data.annotatedArray), std::vector> << std::endl; // alt2: not OK
// std::cout << "val8 is an array or vector: " << std::is_base_of<std::array, decltype(data.val8)>::value << std::endl; // not work because being a template & with NTTPs :-|
data.annotatedArray[2] = 42; // OK -- works as designed
std::cout << "doubleArray content: " << data.doubleArray << std::endl; // OK -- works as designed
//std::cout << "annotatedArray content: " << data.annotatedArray << std::endl; // not OK
std::cout << "annotatedArray content: " << data.annotatedArray.rawValue() << std::endl; // work-around OK but ugly from a users perspective
return data.annotatedArray[2];
}