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.
Deleted template function
void print(auto) = delete;
cons:
Boilerplate for every function
Forbids all implicit conversionsstruct 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
Change interface
struct Base : /* ... */ { /* ... */ static void print(Base); }
cons:
- No operators support (
Base Base
->Base::add(Base, Base)
) - Interface a little bit worse
- 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