Utility Concepts
-
template<typename T>
concept csg::default_arg_initializable A type is “default argument initializable” if it is default initializable and move constructible. This concept is used to enable constructors that take default arguments having a dependent type. For example, consider this simple hash map:
template <typename Key, typename Value, hash_function<const Key &> KeyHash, std::equivalence_relation<const Key &, const Key &> KeyEqual> class example_hash_map { public: // Constructor overload that allows the user to omit arguments for // KeyHash and/or KeyEqual. example_hash_map(std::size_t size, KeyHash h = KeyHash{}, KeyEqual e = KeyEqual{}) requires util::default_arg_initializable<KeyHash> && util::default_arg_initializable<KeyEqual> : m_hash{std::move(h)}, m_keyeq{std::move(e)} {} // class body omitted private: KeyHash m_hash; KeyEqual m_keyeq; };
The above contains a C++11 modernization of a pattern found in the original C++98 STL, e.g., std::map uses the following constructor overload to spare the user from needing to pass a comparator and an allocator most of the time:
template <typename InputIt> map(InputIt first, InputIt second, const Compare &comp = Compare(), const Allocator &alloc = Allocator());
Where the C++98 STL used const-references like
const Compare &compare = Compare(), we use move semantics instead: there are far more “move-only” types than there are “copy-only” types, and the user can easily get copy semantics by passing an lvalue. For example:MyKeyEqualFunctor equal{foo}; example_hash_map<K, V, H, MyKeyEqualFunctor> m{size, {}, equal};
In the above code, the user wants the default-constructed hash function and so passes
{}, an empty initializer list. By the copy/move-elision rules of C++17, this will directly initialize thehargument, and thehargument will be moved intom_hash. On the other hand,equalis an l-value. A copy ofequalwill be made to initialize theeargument, thenewill be moved intom_keyeq.While potentially expensive, a copy is what the user intends here. Note that the const-reference version of this trick (from the C++98 STL) also requires a copy: although binding to the
const &constructor argument is cheap, we must run the copy constructor to construct them_keyeqmember variable inside the map – this cannot be copy-elided.The advantage of the modern version is that all copies can be eliminated if the types support it and the constructor arguments are x-values. C++11 rules also give us the ability to skip an argument just by writing
{}at the call site.To support exotic types that are not default initializable or move constructible, typically the class template in question will provide a constructor overload based on the
csg::can_direct_initializepattern that can be used instead. The disadvantage of that approach compared to this one is that default arguments cannot be used.
-
template<typename T>
concept csg::can_direct_initialize This concept has a simple definition:
template <typename Arg, typename T> concept can_direct_initialize = std::constructible_from<T, Arg>;
It is typically used on constructor templates, to check if a single passed-in argument is able to initialize an instance of the needed type. It differs from std::constructible_from in that the template parameters are in reverse order, so that it can be used to introduce a constrained template parameter for the argument.
Continuing with the
example_hash_mapexample introduced in thecsg::default_arg_initializabledocumentation, the following constructor overload demonstrates how this concept is used:template <typename Key, typename Value, hash_function<const Key &> KeyHash, std::equivalence_relation<const Key &, const Key &> KeyEqual> class example_hash_map { public: template <util::can_direct_initialize<KeyHash> KeyHashInit, util::can_direct_initialize<KeyEqual> KeyEqualInit> example_hash_map(std::size_t size, KeyHashInit &&h, KeyEqualInit &&e) : m_hash{std::forward<KeyHashInit>(h)}, m_keyeq{std::foward<KeyEqualInit>(e)} {} // class body omitted private: KeyHash m_hash; KeyEqual m_keyeq; };
Here,
KeyHashInitandKeyEqualInitare the types of arbitrary objects the user has passed, determined through template argument deduction, that are capable of initializingKeyHashandKeyEqualrespectively.Constructor overloads that use
csg::can_direct_initializetend to be the most “powerful”: they can even work with types that are not default-initializable, move-constructible, or copy-constructible. As long as the type can be direct-initialized by something that is a single argument, that single argument is perfect-forwarded through theexample_hash_mapand the construction happens directly in the member variable.When our
example_hash_mapmust accept more than one dependent type at construction time, e.g., withKeyHashandKeyEqualabove, we normally prefer a constructor based oncsg::default_arg_initializablebecause it allows the user to skip some arguments and use a reasonable default instead. As mentioned above, however, there are some abnormal types that can only work with forwarding construction, so the default argument constructors may be disabled and perfect-forwarding constructors usingcsg::can_direct_initializemust be used instead. The downside of using this more-powerful concept is that default arguments cannot be used: writing a{}will cause template argument deduction to fail.