Tuesday coding tips are super short posts about various tidbits mainly from C++, but also from other programming languages I use. You can also follow the #TuesdayCodingTips hashtag on Mastodon and Linkedin.
Concepts are a C++20 feature that greatly simplifies templated code and allows you to build powerful type trait restrictions.
#include <concepts>
class Interface {
public:
~Interface() = default;
};
template<class T>
concept DerivedFromInterface = std::derived_from<T, Interface>;
// Note how 'class' or 'typename' is replaced with 'DerivedFromInterface'
template<DerivedFromInterface ObjectType>
void DoSomethingWith(ObjectType& object) {}
// Usage
class A final : public Interface {};
A a;
DoSomethingWith(a);
For example, you can create a function that accepts a concrete type that implements a certain interface, potentially allowing the compiler to optimize away the virtual table. Or you can define static interfaces where an object is required to have certain methods or even attributes, even if it doesn’t implement any inheritance-based interface. Or you can restrict the function to be called only with a small subset of well-known types.
#include <concepts>
#include <string>
template<class T>
concept Serializable = requires(T&& object)
{
// Object has callable method 'serialize' which
// returns something that works same as std::string
{ object.serialize()} -> std::same_as<std::string>;
};
template<Serializable T>
void UploadSerializable(const T& object)
{
auto data = object.serialize();
// ...
}
// Usage
class Settings {
public:
std::string serialize() const { return ""; }
};
Settings settings;
UploadSerializable(settings);
Concepts do not replace dynamic polymorphism as you cannot do everything in compile-time. Still, it can open new optimization opportunities and help you write in-place construction functions like std::make_unique without an excessive amount of unreadable templates.
#include <concepts>
#include <string>
#include <memory>
// A function that accepts any string, disregarding if wide or narrow
template<typename T>
requires std::is_same_v<T, std::string> || std::is_same_v<T, std::wstring>
void DoSomethingWithAString(const T& str) {}
// A better implementation of make_unique which checks whether the
// object can be created from given arguments
template<class T, class ... Args>
requires std::constructible_from<T, Args...>
std::unique_ptr<T> make_unique(Args&& ... args)
{
return std::unique_ptr<T>(new T(std::forward<Args>(args)...));
}
Lastly, among the benefits to the syntax, they improve the compiler error messages, making life of your users way simpler.