JHeni
JHeni

Reputation: 751

C++20 use concepts to enforce interfaces on classes

I'm creating a wrapper for a json decoder. Depending on the actual target different json parser 3rd parties are in use. All of them share a common interface.

class IJsonDecoder
{
public:
/**
 * @brief Decodes the json object to a user speicifed type
 * @tparam T Type to decode to
 * @return decoded type
*/
template<typename T>
T decodeTo() const;


/**
 * @brief Returns the value of a specfied key
 * @tparam T Type of the value
 * @param strKey Name of the key. nested keys are possible for ex key.subkey.subsubkey
 * @return Value of the key
*/
template<typename T>
T getKeyValue(const std::string& strKey);
}

Now of course the usal way would be use virtual functions. However virtual functions have some unnecessary run time overhead (I know at compile time which 3rd Party to use).

I already use concepts to enforce some simple interfaces on classes. What I can't figure out is how to make concepts depend on a template parameter itself

///this is a simple interface
template <typename T>
concept ISomeSimpleIF= requires(T a)
{
  {a.data() } -> std::convertible_to<std::string>;  ///< Convertible to std::string
  {a.empty() } -> std::same_as<bool>;  ///< Check if data is empty
}

How can I make this depend on the return value T

///this defines the Interface for IJsonDecoder
template <typename JsonDecoderType_T, typename returnValue_T>
concept IJsonDecoder = requires(JsonDecoderType_T a, returnValue_T b)
{
  {a.decodeTo() } -> std::same_as<returnValue_T >;
  {a.getKeyValue() } -> std::same_as<returnValue_T >;
};

This throws a compiler error missing "<" when applying this concept

  template <IJsonDecoder S>
  void foo(S) {};

Is there a way to make concepts depend on other template parameters? thx for your help

Upvotes: 2

Views: 170

Answers (2)

n. m. could be an AI
n. m. could be an AI

Reputation: 120059

Using CRTP:

template <class Impl>
class IJsonDecoder
{
  public:
    template<typename T> T decodeTo() const
    {
      return (static_cast<const Impl*>(this))->template decodeTo<T>();
    }
    template<typename T> T getKeyValue(const std::string& strKey)
    {
      return (static_cast<Impl*>(this))->template getKeyValue<T>(strKey);        
    }
};

// elsewhere
template <class JsonDecoderImpl>
void someFunction(IJsonDecoder<JsonDecoderImpl>& jsonDecoder) 
{
   std::string s = jsonDecoder.template decodeTo<std::string>(); // e.g.
}

Implementations need to declare themselces like that:

class MyJsonDecoder : public IJsonDecoder<MyJsonDecoder>
{
  // ...
};

In your code that knows the implementation at compile time you can have:

MyJsonDecoder myDecoder;
someFunction(myDecoder);

and this will work, while

someFunction(notADecoder);

will induce a clear, short, user-friendly error message (as opposed to a hailstorm of template-induced error messages otherwise).

No concepts were harmed while producing this answer.

Upvotes: 1

Caleth
Caleth

Reputation: 63142

The concept IJsonDecoder has two type parameters. When you use it as a template type constraint, the first is supplied for you, You still have to supply the second.

template <IJsonDecoder<SomeConcreteTypeThatFooExtracts> S>
void foo(S) {};

This does highlight that your concept IJsonDecoder doesn't match your original class IJsonDecoder. The class promises to be able to convert to every type, whereas the concept promises to convert to one type.

There is no concept that corresponds to your class, because that requires universal quantification.

Here is a concept that creates a family of constraints that your class satisfies.

template <typename JsonDecoderType_T, typename returnValue_T>
concept IJsonDecoder = requires(JsonDecoderType_T a, std::string b)
{
  { a.decodeTo<returnValue_T>() } -> std::same_as<returnValue_T >;
  { a.getKeyValue<returnValue_T>(b) } -> std::same_as<returnValue_T >;
};

Upvotes: 1

Related Questions