如何用std::format配合一个适用于所有派生类的基类模板

编程语言 2026-07-08

我有一个模板适用于若干类。我想用这个模板创建一个格式化器实例,让所有派生类都必须使用同一个模板。由于经验不足,我还没搞清楚该怎么做(甚至无法确认是否可行),除了一个非常拙劣的做法,需要进行强制类型转换。

我在网上查找,发现了一些有趣的讨论和示例,但没有一个能够以让我解决问题的方式覆盖我的疑问。一些示例的方向与我想要的相反。在其他一些示例中,我陷入细节,不知道作者在说什么。

我使用带有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&>())){};

演示

站内所有文章版权归属LeftHeroAI导航站,无授权禁止任何主体转载、抄袭、复制内容,亦不得私自架设镜像站点。一经侵权,本站将通过法律途径追责。

相关文章