Home > Enterprise >  C require function without implicit conversion
C require function without implicit conversion

Time:03-08

I'm using boost::variant to imitate inheritance with value semantics.

There is one class that may be printed:

struct Printable { /* ... */ };

void print(const Printable &) { /* ... */ }

And class that may not:

struct NotPrintable { /* ... */ };

Finally, there is "Base" class with implicit cast:

struct Base : boost::variant<Printable, NotPrintable>
{
    Base(const Printable &) {}    // constructor for implicit cast
    Base(const NotPrintable &) {} // constructor for implicit cast
};

// Print, if printable, throw exception, if not
void print(const Base &base) 
{ 
    Printer printer{};
    base.apply_visitor(printer);
}

The problem is how to check for printable inside of visitor:

struct Printer
{
   using result_type = void;

   // If printable 
   template<typename PrintableType> requires 
   requires(const PrintableType &v) { {print(v)}; }  // (1)
   void operator()(const PrintableType &v) { print(v); }

   // If not printable
   void operator()(const auto &v) { throw /*...*/; }
}; 

Requirement (1) is always true due-to implicit conversion to const Base &. How to avoid conversion only in that exact place?

CodePudding user response:

As @Jarod42 said in the comments, in order to ensure that implicit conversion does not occur, you need to define a template print() function to "absorb" other types (NotPrintable in your example) and set it to delete

void print(const auto&) = delete;

Then the printable concept can be defined as

template<class T>
concept printable = requires (const T& x) { print(x); };

which requires the expression print(x) to be well-formed.

When the type of T is Printable or Base, the expression print(x) is valid since you have defined the corresponding print() function for them. When the type of T is NotPrintable or other types, the deleted print() will be invoked which makes the expression ill-formed, so that the constraint is not satisfied.

Then you can use this concept to constrain Printer::operator()

struct Printer {
  using result_type = void;

  // If printable 
  template<printable PrintableType>
  void operator()(const PrintableType& v) { print(v); }

  // If not printable
  void operator()(const auto&) { throw /*...*/; }
};

Note that since print(const auto&) can be instantiated to any type, this will prohibit all implicit conversions, but we can still allow some implicit conversions by adding constraints to it

#include <concepts>

struct Boolean { };
struct Integer {
  Integer();
  Integer(Boolean);
  friend Integer operator (Integer, Integer);
};
struct String { };

template<class T, class U>
  requires (!std::convertible_to<U, T>)
void operator (T, U) = delete;

int main() {
  Integer{}   Integer{}; // OK
  Integer{}   String{};  // ERROR (as expected)
  Integer{}   Boolean{}; // OK
};

CodePudding user response:

Currently, I don't see the most perfect solution, but there are some workaround.

  1. Deleted template function

    void print(auto) = delete;
    

cons:

  • Boilerplate for every function

  • Forbids all implicit conversions

    struct Boolean;
    
    struct String;
    
    struct Integer
    {
      Integer(Boolean);
    
      friend Integer operator (Integer, Integer);
    }
    
    template<class T, class U>
    requires (!std::convertible_to<U, T>)
    void operator (T, U) = delete;
    
    Integer   Integer // OK
    Integer   String  // ERROR (as expected)
    Integer   Boolean // OK
    
  1. Change interface

    struct Base : /* ... */
    {
      /* ... */
    
      static void print(Base);
    }
    

cons:

  • No operators support (Base Base -> Base::add(Base, Base))
  • Interface a little bit worse
  1. Add traits
    template<typename T> struct Traits {
      static constexpr bool is_printable = true;
    }
    

cons:

  • Boilerplate for every class and method
  • How to handle implicit conversion (Boolean Integer)?
  • Closed for extension
  • Related