Home > OS >  Avoiding implicit conversion with concepts
Avoiding implicit conversion with concepts

Time:10-16

Since templates and dynamic polymorphism don't mix well, I am currently designing a concept, instead of an interface (implemented with abstract class), for a Loggable type, which supports operations:

logger.log(LogLevel::info) << "some message" << 0 << 0.0 << 'c';

Provided the log levels defined:

enum class LogLevel
{
    info,
    warning,
    error
};

The concept looks like that:

template<typename T>
concept Loggable = requires(T v)
{
    {
        v.log(LogLevel{})
        } -> std::convertible_to<T&>;

    {
        v << "log message" << static_cast<unsigned>(0) << static_cast<int>(0) << static_cast<float>(0.0)
          << static_cast<unsigned char>(0) << static_cast<char>('0')
        } -> std::convertible_to<T&>;
};

To test a logger, the following function has been defined:

template<typename T>
requires Loggable<T>
void fun(T& v)
{
    v.log(LogLevel::error);
}

I have defined a Logger:

struct Logger1
{
    Logger1& log(LogLevel)
    {
        return *this;
    }

    Logger1& operator<<(float)
    {
        return *this;
    }

    Logger1& operator<<(std::string)
    {
        return *this;
    }
};

Then invoking the Logger with:

Logger1 l1{};
fun(l1);

Gives no compiler errors. This is because the string literal is implicitly cast to std::string and unsigned, int, char, unsigned char get eventually implicitly cast to float.

How can I:

  1. Forbid implicit cast of int, char, unsigned char, unsigned to float, but ...
  2. Still allow implicit cast of the string literal to std::string?

CodePudding user response:

You can do this at the logger level by creating a deleted overload that catches all, but only, the types you want to exclude from implicitly converting:

#include <concepts>
#include <string>

// Probably not 100% correct, mostly for the example...
template <typename T>
concept Arithmetic = std::integral<T> || std::floating_point<T>;

struct Logger1
{
    template<Arithmetic T>
    Logger1& operator<<(T) = delete;

    Logger1& operator<<(float)
    {
        return *this;
    }

    Logger1& operator<<(std::string)
    {
        return *this;
    }
};

int main() {
    Logger1 log;

    log << "AAA"; // Converts implicitly to std::string.
    log << 12.5f; // handled by the explicit float overload.
    log << 3;  // Compile error here.
}

CodePudding user response:

To avoid implicit conversion, we can define a template operator<< that matches all other types.

In addition, in order to avoid blocking string literal, we can add a constraint for this operator<< to ensure that the operand cannot be converted to std::string:

#include <string>

template<typename T>
concept WeaklyLoggable = requires(T v) {
  { v.log(LogLevel{}) } -> std::convertible_to<T&>;
};

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

template<typename T>
concept Loggable = WeaklyLoggable<T> && requires(T v) {
  { 
     v << "log message"
       << static_cast<unsigned>(0) 
       << static_cast<int>(0) 
       << static_cast<float>(0.0) 
       << static_cast<unsigned char>(0) 
       << static_cast<char>('0')
  } -> std::convertible_to<T&>;
};

Demo.

  • Related