code/include/swoc/swoc_meta.h (57 lines of code) (raw):
// SPDX-License-Identifier: Apache-2.0
// Copyright Verizon Media 2020
/** @file
Meta programming support utilities.
*/
#pragma once
#include <type_traits>
#include <utility>
#include "swoc/swoc_version.h"
namespace swoc { inline namespace SWOC_VERSION_NS { namespace meta {
/** This creates an ordered series of meta template cases that can be used to select one of a set
* of functions in a priority ordering. A set of templated overloads take an (extra) argument of
* the case structures, each a different one. Calling the function invokes the highest case that
* is valid. Because of SFINAE the templates can have errors, as long as at least one doesn't.
* The root technique is to use @c decltype to check an expression for the overload to be valid.
* Because the compiler will evaluate everything it can while parsing the template this
* expression must be delayed until the template is instantiated. This is done by making the
* return type @c auto and making the @c decltype dependent on the template parameter. In
* addition, the comma operator can be used to force a specific return type while also checking
* the expression for validity. E.g.
*
* @code
* template <typename T> auto func(T && t, CaseTag<0>) -> decltype(t.item, int()) { }
* @endcode
*
* The comma operator discards the type and value of the left operand therefore the return type of
* the function is @c int but this overload will not be available if @c t.item does not compile
* (e.g., there is no such member). The presence of @c t.item also prevents this compilation check
* from happening until overload selection is needed. Therefore if the goal was a function that
* would return the value of the @c T::count member if present and 0 if not, the code would be
*
* @code
* template <typename T> auto Get_Count(T && t, CaseTag<0>)
* -> int
* { return 0; }
* template <typename T> auto Get_Count(T && t, CaseTag<1>)
* -> decltype(t.count, int())
* { return t.count; }
* int Get_Count(Thing& t) { return GetCount(t, CaseArg); }
* @endcode
*
* Note the overloads will be checked from the highest case to the lowest and the first one that
* is valid (via SFINAE) is used. This is the point of using the case arguments, to force an
* ordering on the overload selection. Unfortunately this means the functions @b must be
* templated, even if there's no other reason for it, because it depends on SFINAE which doesn't
* apply to normal overloads.
*
* The key point is the expression in the @c decltype should be the same expression used in the
* method to verify it will compile. It is annoying to type it twice but there's not a better
* option.
*
* Note @c decltype does not accept explicit types - to have the type of "int" a function returning
* @c int must be provided. This is easy for simple builtin types such as @c int - use the
* constructor @c int(). For @c void and non-simple types (such as @c int* ) this is a bit more
* challenging. A general utility is provided for this - @c TypeFunc. For the @c void case this
* would be <tt>decltype(TypeFunc<void>())</tt>. For @c int* it would be
* <tt>decltype(TypeFunc<int *>())</tt>.
*/
/// Case hierarchy.
template <unsigned N> struct CaseTag : /** @cond DOXYGEN_FAIL */ public CaseTag<N - 1> /** @endcond */ {
constexpr CaseTag() {}
static constexpr unsigned value = N; ///< Case tag value.
};
/// Case hierarchy anchor.
template <> struct CaseTag<0> {
constexpr CaseTag() {}
static constexpr unsigned value = 0; ///< Case tag value.
};
/** This is the final case - it forces the class hierarchy.
* After defining the cases using the indexed case arguments, this is used to perform the call.
* To increase the hierarchy depth, change the template argument to a larger number.
*/
static constexpr CaseTag<9> CaseArg{};
/** A typed function for use in @c decltype.
*
* @tparam T The desired type.
* @return @a T
*
* This function has no implementation. It should be used only inside @c decltype when a specific
* type (rather than the type of an expression) is needed. For a type @a T that has a expression
* default constructor this can be used.
*
* @code
* decltype(T())
* @endcode
*
* But if there is no default constructor this will not compile. This is a work around, so the
* previous expression would be
*
* @code
* decltype(meta::TypeFunc<T>())
* @endcode
*
* Note this can also be a problem for even built in types like @c unsigned @c long for which
* the expression
*
* @code
* decltype(unsigned long())
* @endcode
*
* does not compile.
*/
template <typename T> T TypeFunc();
/** Template parameter eraser.
*
* @tparam T Parameter to erase (pass explicitly)
* @tparam U Parameter for forwarded value (implicit)
* @param u Forward value
* @return @a u
*
* This has no effect on @a u but fools the compiler in to thinking @a T has been used.
* This avoids metaprogramming issues with unused template parameters.
*
* Suppose an API has changed a function from "cheer_for_delain" to "cheer_delain". To handle this
* the metacase support is used, but there is no apparent template parameter. A fake one can be used
* and defaulted to @c void. But that creates the unused template parameter warning. This is fixed
* by doing something to erase the template parameter @a V while forwarding parameter @a x. The result
* is a function @c f that calls the correct function automatically. Note this can't be in the body of
* the function because even for SFINAE the function body must compile and the variant with the wrong
* function will fail.
*
* @code
* template <typename V = void>
* auto f(UDT x, swoc::meta::CaseTag<0>) -> decltype(cheer_for_delain(eraser<V>(x)))
* { return cheer_for_delain(eraser<V>(x)); }
*
* template <typename V = void>
* auto f(UDT x, swoc::meta::CaseTag<1>) -> decltype(cheer_delain(eraser<V>(x)))
* { return cheer_delain(eraser<V>(x)); }
*
* f(x, swoc::meta::CaseArg); // Invoke the correctly named function
* @endcode
*/
template <typename T, typename U>
constexpr auto
eraser(U &&u) -> U {
return std::forward<U>(u);
}
/** Support for variable parameter lambdas.
*
* @tparam Args Lambdas to combine.
*
* This creates a class that has an overloaded function operator, with each overload corresponding
* to one of the provided lambdas. The original use case is with @c std::visit where this can be
* used to construct the @a visitor from a collection of lambdas.
*
* @code
* std::variant<int, bool> v;
* std::visit(swoc::meta::vary{
* [] (int & i) { ... },
* [] (bool & b) { ... }
* }, v);
* @endcode
*/
template <typename... Args> struct vary : public Args... {
using Args::operator()...;
};
/// Template argument deduction guide (C++17 required).
template <typename... Args> vary(Args...) -> vary<Args...>;
/** Check if a type is any of a set of types.
*
* @tparam T Type to check.
* @tparam Types Type list to match against.
*
* @a value is @c true if @a T is the same as any of @a Types. This is commonly used with
* @c enable_if to enable a function / method only if the argument type is one of a fixed set.
* @code
* template < typename T > auto f(T const& t)
* -> std::enable_if_t<swoc::meta::is_any_of<T, int, float, bool>::value, void>::value
* { ... }
*/
template <typename T, typename... Types> struct is_any_of {
static constexpr bool value = std::disjunction<std::is_same<T, Types>...>::value;
};
template <typename T, typename... Types> struct is_homogenous {
static constexpr bool value = std::conjunction<std::is_same<T, Types>...>::value;
using type = T;
};
/// Helper variable template for is_any_of
template <typename T, typename... Types> inline constexpr bool is_any_of_v = is_any_of<T, Types...>::value;
/** Type list support class.
*
* @tparam Types List of types.
*
* The examples for the nested meta functions presume a type list has been defined like
* @code
* using TL = swoc::meta::type_list<int, bool, float, double>;
* @endcode
*/
template <typename... Types> struct type_list {
/// Length of the type list.
static constexpr size_t size = sizeof...(Types);
/** Define a type using the types in the list.
* Give a type list, defining a variant that has those types would be
* @code
* using v = TL::template apply<std::variant>;
* @endcode
*
* The primary reason to do this is if the type list is needed elsewhere (e.g. to enable
* methods only for variant types).
*
* @see contains
*/
template <template <typename... Pack> typename T> using apply = T<Types...>;
/** Determine if a type @a T is a member of the type list.
*
* @tparam T Type to check.
*
* Frequently used with @c enable_if. This is handy for variants, if the list of variant
* types is encoded in the type list.
*
* @code
* template < typename T > auto f(T const& t)
* -> std::enable_if_t<TL::template contains<T>::value, void>
* { ... }
* @endcode
*/
template <typename T> static constexpr bool contains = is_any_of<T, Types...>::value;
};
/** Scoped value change.
*
* The purpose of this class is to change the value of a variable in a scope and then change it
* back when the scope is exited. The old value will be moved to a cache variable and then moved
* back when the instance is destructed. This is very useful to temporarily tweak global variables
* which having to know what the current value is.
*
* @code
* {
* let save(var, value);
* // var now has value.
* }
* // var now has original value.
* @endcode
*
* @tparam T Type of variable to scope.
*/
template <typename T> struct let {
using self_type = let;
let(self_type const &that) = delete;
self_type &operator=(self_type const &) = delete;
T &_var; ///< Reference to scoped variable.
T _value; ///< Original value.
/** Construct a scope.
*
* @param var Variable to scope.
* @param value temporary value to assign.
*/
let(T &var, T const &value);
/** Construct a scope.
*
* @param var Variable to scope.
* @param value temporary value to assign.
*/
let(T &var, T &&value);
~let();
};
template <typename T> let<T>::let(T &var, T const &value) : _var(var), _value(std::move(var)) {
_var = value;
}
template <typename T> let<T>::let(T &var, T &&value) : _var(var), _value(std::move(var)) {
_var = std::move(value);
}
template <typename T> let<T>::~let() {
_var = std::move(_value);
}
}}} // namespace swoc::SWOC_VERSION_NS::meta