如何用std::format配合一个适用于所有派生类的基类模板
我有一个模板适用于若干类。我想用这个模板创建一个格式化器实例,让所有派生类都必须使用同一个模板。由于经验不足,我还没搞清楚该怎么做(甚至无法确认是否可行),除了一个非常拙劣的做法,需要进行强制类型转换。
我在网上查找,发现了一些有趣的讨论和示例,但没有一个能够以让我解决问题的方式覆盖我的疑问。一些示例的方向与我想要的相反。在其他一些示例中,我陷入细节,不知道作者在说什么。
我使用带有GCC和 CMake的 Debian trixie,但我的问题并不限于这个特定环境。
我做了测试示例,并尽量把代码保持简洁,只展示必要部分,避免那些与问题无关的冗余。
简而言之,以下是我想要做的“伪代码”:
class Base<T>;
class DerivedFp : public Base<double>;
class DerivedInt : public Base<int>;
class DerivedBool : public Base<bool>;
template<???>
struct std::formatter<derived> : std::formatter<std::string> {
template<class FmtContext>
auto format(const derived & d, FmtContext & ctx) const {
// do the same formatting for each derived instance.
}
};
一个不涉及模板、仅使用类实现的我的想法的一个示例是这样的:
#include <iostream>
#include <concepts>
#include <type_traits>
#include <format>
struct CBase {
CBase(double arg1, double arg2) : x(arg1), y(arg2) {}
double x, y;
virtual unsigned int get_precision() const { return 6; }
};
struct CDpub : public CBase {
CDpub(double arg1, double arg2) : CBase(arg1, arg2) {}
};
struct CDpri : private CBase {
CDpri(double arg1, double arg2) : CBase(arg1, arg2) {}
};
template<std::derived_from<CBase> derived>
struct std::formatter<derived> : std::formatter<std::string> {
template<class FmtContext>
auto format(const derived & d, FmtContext & ctx) const {
auto p{ d.get_precision() };
return formatter<string>::format(std::format("({:.{}f}, {:.{}f})", d.x, p, d.y, p), ctx);
}
};
int main(int argc, char **argv) {
std::cout << "Hello, world!" << std::endl;
auto base{ CBase( 1.1, 2.2 ) };
auto dpub{ CDpub( 11.11, 22.22 ) };
auto dpri{ CDpri( 0.0, 1.1 ) };
std::cout << "base = " << std::format("{}", base) << std::endl;
std::cout << "dpub = " << std::format("{}", dpub) << std::endl;
// the next line, when enabled generates a compile time error, which is exactly as expected.
//std::cout << "dpri = " << std::format("{}", dpri) << std::endl;
// error: static assertion failed: std::formatter must be specialized for each type being formatted
std::cout << "Goodby, world!" << std::endl;
return 0;
}
类 std::derived_from 正在完成我对类的全部需求。虽然cppreference.net只给出基类的细节,但将其扩展到基模板并不困难。下面给出一个能够编译并按预期运行的示例:
template<typename T>
class A { T value; };
class B : public A<int> {};
class C : private A<double> {};
// std::derived_from == true only for public inheritance or exact same class
static_assert(std::derived_from<B, B> == true); // same class: true
static_assert(std::derived_from<int, int> == false); // same primitive type: false
static_assert(std::derived_from<B, A<int>> == true); // public inheritance: true
static_assert(std::derived_from<C, A<double>> == false); // private inheritance: false
// std::is_base_of == true also for private inheritance
static_assert(std::is_base_of_v<B, B> == true); // same class: true
static_assert(std::is_base_of_v<int, int> == false); // same primitive type: false
static_assert(std::is_base_of_v<A<int>, B> == true); // public inheritance: true
static_assert(std::is_base_of_v<A<double>, C> == true); // private inheritance: true
int main(int argc, char **argv) {
std::cout << "Hello, world!" << std::endl;
return 0;
}
有一个版本可以按我想要的方式工作,但在每次格式化尝试时都需要一个非常丑陋的强制转换。下面是它的代码。
#include <iostream>
#include <concepts>
#include <type_traits>
#include <format>
template <typename T, unsigned int D = 6>
requires std::is_arithmetic_v<T>
struct Base {
Base(T arg1, T arg2) : x(arg1), y(arg2) {}
T x, y;
virtual unsigned int get_precision() const { return D; }
};
struct DerivedFp : Base<double> {
DerivedFp(double arg1, double arg2) : Base(arg1, arg2) {}
};
struct DerivedInt : Base<int> {
DerivedInt(int arg1, int arg2) : Base(arg1, arg2) {}
};
struct DerivedBool : Base<bool> {
DerivedBool(bool arg1, bool arg2) : Base(arg1, arg2) {}
};
template<typename T, unsigned int D>
struct std::formatter<Base<T, D>> : std::formatter<std::string> {
template<class FmtContext>
auto format(const Base<T, D> & b, FmtContext & ctx) const {
if constexpr (std::is_floating_point_v<T>) {
auto d{ b.get_precision() };
return formatter<string>::format(std::format("({:.{}f}, {:.{}f})", b.x, d, b.y, d), ctx);
}
return formatter<string>::format(std::format("({}, {})", b.x, b.y), ctx);
}
};
int main(int argc, char **argv) {
std::cout << "Hello, world!" << std::endl;
auto f{ DerivedFp( 1.1, 2.2 ) };
auto i{ DerivedInt( 1, 2 ) };
auto b{ DerivedBool( true, false ) };
std::cout << "f = " << std::format("{}", static_cast<Base<double, 6>>(f)) << std::endl;
std::cout << "i = " << std::format("{}", static_cast<Base<int, 6>>(i)) << std::endl;
std::cout << "b = " << std::format("{}", static_cast<Base<bool, 6>>(b)) << std::endl;
std::cout << "Goodby, world!" << std::endl;
return 0;
}
上面的示例都在使用某种形式的 template<std::derived_from<CBase> derived> struct std::formatter<derived>...。这是核心部分(我想)。
我用 std::is_base_of_v 和一个模板基类所做的测试表明,应该有可能实现某种方法。所有使用模板基类的测试都让我觉得模板参数是关键问题,但我缺乏必要的知识和洞察力,无法准确定位出问题所在。
我尝试了几种配置,结果都失败,有些有我能理解的明确原因,有些……直接失败,我也不知道为什么。下面是一系列尝试的清单,包含那些愚蠢而显而易见的做法。
这是我的问题:
- 是否可以在
template<std::derived_from<Base<T, D>> derived> struct std::formatter<derived>...中使用模板基类?如果不行,那就到此为止。 - 如果可以实现,那么应该怎么做?
欢迎任何建议(也包括对“这是不可能”的解释)。
下面是我尝试过的方法。不是完整的代码,只列出与上面示例不同的关键行。下面的代码用于表达我试图遵循的思路。
template <typename U, typename T, unsigned int D>
concept IsDerivedFromBase = std::derived_from<U, Base<T, D>>;
template<IsDerivedFromBase derived> // error: wrong number of template arguments (1, should be 3)
struct std::formatter<derived> : std::formatter<std::string> {
template<typename T, unsigned int D, std::derived_from<Base<T, D> derived> // error: parse error in template argument list
struct std::formatter<derived> : std::formatter<std::string> {
template<
typename derived,
typename T,
unsigned int D,
typename = std::enable_if_t<std::is_base_of_v<Base<T, D>, derived>
>
struct std::formatter<derived> : std::formatter<std::string> { // error: too few template-parameter-lists
template<
typename T,
unsigned int D,
std::same_as<Base<T, D>> derived
>
struct std::formatter<derived> : std::formatter<std::string> { // error: too few template-parameter-lists
解决方案
正如你似乎已经注意到的,部分特化的匹配需要严格匹配。你的派生类型并不完全等同于 Base<T, D>。
由于所有派生类具有相同的模式,可以改用 using:
using DerivedFp = Base<double>;
using DerivedInt = Base<int>;
using DerivedBool = Base<bool>;
我怀疑上述做法长期看可能不够令人满意;如果你确实需要从 Base<T, D> 派生的类,请通过继承你已经定义的 std::formatter<Base<T, D>>,让它们全部对 std::formatter 进行特化:
// this function template can deduce the template parameters when given
// a derived type:
template <class U, unsigned int E>
auto Base_extractor(const Base<U, E>&) -> Base<U, E>; // never defined
// std::formatter specializations for all types derived from Base<T, D>
template <class T>
requires requires(const T& t) { Base_extractor(t); } // constrain it
struct std::formatter<T> // derive from the Base<T, D> formatter:
: std::formatter<decltype(Base_extractor(std::declval<const T&>()))> {};
一个可能更易读、但实现同样效果的替代方案:
template <class U, unsigned int E>
auto Base_extractor(const Base<U, E>&) -> std::formatter<Base<U, E>>;
template <class T>
requires requires(const T& t) { Base_extractor(t); }
struct std::formatter<T>
: decltype(Base_extractor(std::declval<const T&>())){};