专业的编程技术博客社区

网站首页 > 博客文章 正文

使用concept替换CRTP的静态多态(concept2114s)

baijin 2024-10-12 02:09:12 博客文章 15 ℃ 0 评论

CRTP的用途之一是实现静态多态性。这种技术可以用来为库中的类提供定制点。虽然CRTP是实现静态接口和向类添加功能的有力工具,但它有一些缺点,我们可以通过使用C++ 20 concept来做得更好。

使用CRTP的小示例

我们将创建一些函数,该函数接受多态Logger并将std::string_view消息记录到所有日志级别。为了简单起见,我们的玩具logger没有日志级别过滤或接收器的概念。我们还将创建CRTP基类:

template <typename TLoggerImpl>
class Logger {
public:
  void LogDebug(std::string_view message) {
    Impl().DoLogDebug(message);
  }
  void LogInfo(std::string_view message) {
    Impl().DoLogInfo(message);
  }
  void LogError(std::string_view message) {
    Impl().DoLogError(message);
  }
private:
  TLoggerImpl& Impl() { return static_cast<TLoggerImpl&>(*this); }
  friend TLoggerImpl;
};

template <typename TLoggerImpl>
void LogToAll(Logger<TLoggerImpl>& logger, std::string_view message) {
  logger.LogDebug(message);
  logger.LogInfo(message);
  logger.LogError(message);
}

让我们定义一对派生的记录器类,我们将其称为CustomLogger和TestLogger:

struct CustomLogger : public Logger<CustomLogger> {
  void DoLogDebug(std::string_view message) const {
    std::cout << “[Debug] ” << message << ‘\n’;
  }
  void DoLogInfo(std::string_view message) const {
    std::cout << “[Info] ” << message << ‘\n’;
  }
  void DoLogError(std::string_view message) const {
    std::cout << “[Error] ” << message << ‘\n’;
  }
};

struct TestLogger : public Logger<CustomLogger> {
  void DoLogDebug(std::string_view) const {}
  void DoLogInfo(std::string_view) const {}
  void DoLogError(std::string_view) const {}
};

现在我们可以这样使用:

CustomLogger custom_logger;
LogToAll(custom_logger, “Hello World”);
TestLogger test_logger;
LogToAll(test_logger, “Hello World”);

此代码正常工作,但存在以下问题:

  • 派生类中的方法的名称必须与基类中的方法不同;如果它们使用相同的名称,则基类接口将被派生类中的方法隐藏
  • 有一些CRTP固有的间接(通过基类调用继承类)
  • 它没有清楚地表达它限制制定Logger API的意图

CRTP习惯用法的一个更紧迫的问题是它是一个习惯用法。当你试图理解一段代码时,你必须时刻注意这个模式。仅仅浏览一下Logger代码,它可能无法立即看出它要完成什么,除非这是您经常遇到的事情。

现在我们知道了问题所在,我们将迭代重构我们的示例,使用concept来修复问题。

Requires Requires Requires….

首先,我们将从Logger内部删除所有代码。剩下的是:

template <typename TLoggerImpl>
struct Logger {};

我们现在要做的是向TLoggerImpl添加约束。忽略concept,我们可以通过即席约束来实现:

template <typename TLoggerImpl>
  requires requires(TLoggerImpl logger) {
    logger.LogDebug(std::string_view{});
    logger.LogInfo(std::string_view{});
    logger.LogError(std::string_view{});
  }
struct Logger {};

两个requires关键字具有不同的含义。左边的是一个requires子句,它检查(requires)右边的requires表达式的计算结果是否为true。

我们还希望将传递的模板参数中的功能公开给Logger,如果它满足其约束。为此,我们将允许Logger从TLoggerImpl继承。现在我们有了以下内容:

template <typename TLoggerImpl>
  requires requires(TLoggerImpl logger) {
    ...
  }
struct Logger : TLoggerImpl {};

消除即席约束

们给自己制造了一个新问题。使用requires requires让人感觉,而且可能就是,一种代码异味。requires表达式应该重构为一个concept,那我们就这么做。我们将这个concept称为LoggerLike,它表示满足它的任何东西都像记录器应该看起来的样子。

template <typename TLoggerImpl>
concept LoggerLike = requires(TLoggerImpl log) {
  log.LogDebug(std::string_view{});
  log.LogInfo(std::string_view{});
  log.LogError(std::string_view{});
};

template <typename TLoggerImpl> requires LoggerLike<TLoggerImpl>
struct Logger : TLoggerImpl {};

更妙的是,我们可以删除requires子句,并在模板参数列表中使用该概念作为类型约束,如下所示:

template <LoggerLike TLoggerImpl> 
struct Logger : TLoggerImpl {};

这实际上类似于将概念用作纯虚拟基接口,但在这里,这是在编译时解析的静态接口。此接口本身没有功能;它只定义其模板参数必须实现的方法。

此时,我们应该修改CustomLogger和TestLogger类。我们将删除继承并重命名它们的方法,以符合我们的concept:

struct CustomLogger {
  void LogDebug(std::string_view message) const {
    std::cout << “[Debug] ” << message << ‘\n’;
  }
  void LogInfo(std::string_view message) const {
    std::cout << “[Info] ” << message << ‘\n’;
  }
  void LogError(std::string_view message) const {
    std::cout << “[Error] ” << message << ‘\n’;
  }
};

struct TestLogger {
  void LogDebug(std::string_view) const {}
  void LogInfo(std::string_view) const {}
  void LogError(std::string_view) const {}
};

您可能已经注意到,我们没有对LogToAll函数进行任何修改。它仍然接受一个Logger&:

template <typename TLoggerImpl>
void LogToAll(Logger<TLoggerImpl>& logger, std::string_view message) {
  logger.LogDebug(message);
  logger.LogInfo(message);
  logger.LogError(message);
}

让我们为每个记录器创建别名。为了实现这一点,我们还将使用Impl后缀重命名日志记录器(它们也可以在名称空间中限定):

struct CustomLoggerImpl { … };

struct TestLoggerImpl { … };

using CustomLogger = Logger<CustomLoggerImpl>;
using TestLogger = Logger<TestLoggerImpl>;

现在我们可以像以前一样使用它们:

CustomLogger custom_logger;
LogToAll(custom_logger, “Hello World”);
TestLogger test_logger;
LogToAll(test_logger, “Hello World”);

我们现在重构了我们的示例以使用concept,与我们开始时相比,它更简单:

  • 我们已经修复了方法命名问题;concept在设计上强制使用方法名
  • 我们消除了一些间接,因为我们不再需要在基类和派生类中实现功能
  • 我们的代码现在更具表现力,因为概念的存约束了语法和语义;我们现在知道我们正在试图约束我们的Logger

再进一步

有没有办法让它变得更简单?我们这里还有一些冗余。我们使用Logger类来强制我们的concept,而不是直接使用它。我的意思是我们的函数可以这样写:

template <LoggerLike TLogger>
void LogToAll(TLogger& logger, std::string_view message) {
  logger.LogDebug(message);
  logger.LogInfo(message);
  logger.LogError(message);
}

这样就不需要Logger类和类型别名。我们还可以将logger类重新命名为TestLogger和CustomLogger,并直接使用它们。我们使用类和函数的方式保持不变:

CustomLogger custom_logger;
LogToAll(custom_logger, “Hello World”);
TestLogger test_logger;
LogToAll(test_logger, “Hello World”);

这样做的目的是将约束检查从创建别名的位置移动到将其传递给需要该concept的API的位置。根据您的用例,您可以决定使用其中一个。

增加功能

在转换到概念之后,向我们的记录器添加功能应该非常容易。快速想象一下,我们想在所有日志中添加一些tag。让我们再看看CustomLoggerImpl类:

struct CustomLoggerImpl {
  void LogDebug(std::string_view message) const {
    std::cout << “[Debug] ” << message << ‘\n’;
  }
  void LogInfo(std::string_view message) const {
    std::cout << “[Info] ” << message << ‘\n’;
  }
  void LogError(std::string_view message) const {
    std::cout << “[Error] ” << message << ‘\n’;
  }
};

要向CustomLoggerImpl和任何其他满足LoggerLike的记录器添加功能,只需将其直接添加到派生类中,如下所示:

template <LoggerLike TLoggerImpl>
struct TaggedLogger : TLoggerImpl {
  TaggedLogger(const std::string& tag) : m_tag(tag) {}

  void LogDebugTagged(const std::string& message) {
    const std::string& tagged = “[” + m_tag + "] " + message;
    static_cast<TLoggerImpl*>(this)->LogDebug(tagged);
  }
  ...
private:
  std::string m_tag;
};

using TaggedCustomLogger = TaggedLogger<CustomLoggerImpl>;

可以这样使用:

TaggedCustomLogger logger;
logger.SetTag(“MyTag”);
logger.LogDebugTagged(“Hello World”);

Concept会改变我们的代码方式

CRTP是自从C++ 98以来一直以来我们使用的一个很好的旧模板技巧,现在它已经被概念化了。

concept将改变我们编写模板代码的方式。就像模板本身一样,这些年来展示了它们的力量,concept可能有一些有趣的技术有待发现。

您是如何使用concept以便让您的模版代码更简单呢?

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表