如何检查模板类型名称是否与另一种类型相同但忽略模板参数

How to check if a template typename is the same as another type but ignoring template parameters

提问人:FalcoGer 提问时间:11/3/2023 最后编辑:cpplearnerFalcoGer 更新时间:11/3/2023 访问量:95

问:

我想制作一个提供测量单位的库,例如长度、质量等作为类型。我知道这样的库可能已经存在了,但我想学习如何自己做这样的事情。我从 中汲取了灵感,它使用了 ,所以我也使用了这种方法。但是我遇到了一个问题,即存在大量代码重复,特别是对于数学运算,例如.std::chrono::durationstd::ratio+

我的方法是,我有一个基类,我从中派生出单独的测量值,例如 或 。这些单独的测量值之间的许多代码是相同的,例如 ,它采用两个测量值并将值相加,以校正它们之间的比率。例如,我可以将 5 米增加到 12 英尺。但是我不能把它放在基类中,因为如果我这样做了,我可以在长度上添加一个质量,这是无稽之谈,而这正是我首先试图纠正的事情。MeasurementLengthMassoperator+

我的解决方案是将这些东西放在基类中,但添加一个 requires 子句,禁止 的类型与要添加到的对象的类型不同。
问题在于,我仍然希望将一个长度单位添加到另一个长度单位,但使用不同的模板参数。但是 c++ 将它们视为不同的类型,因此如果我使用 ,它会失败,阻止我将 5 米添加到 12 英尺,因为 (1 米)和 (1 英尺) 是不同的类型。
otherstd::ratiostd::same_asLength<float, std::ratio<1>>Length<float, std::ratio<100'000, 328'084>>

我怎样才能确保

using Kilogram = Measurements::Mass<float>;
using Meter = Measurements::Length<float>;
using Feet = Length<float, std::ratio<100'000, 328'084>>;

Meter m{5};
Kilogram kg{45};

// m + kg; // should error
Meter m2{Feet{22}}; // should work
Feet f1 = m2; // should work
// Feet f2 = 12; // should error
Meter m3 = m2 + ft; // should work
// Kilogram kg2 = m2; // should error

但是没有复制 Mass 和 Length 中的所有操作重载和构造函数?

下面是问题的示例

#include <iostream>
#include <ratio>
#include <type_traits>
#include <utility>

namespace Measurements
{

template <typename T>
concept IsArithmetic = requires { requires std::is_arithmetic_v<T>; };

template <typename R>
concept IsRatio = requires {
    {
        R::num
    };
    {
        R::den
    };
};

template <IsArithmetic Rep, IsRatio Factor = std::ratio<1, 1>>
class Measurement
{
  private:
    Rep m_value {};

  public:
    using REP    = Rep;
    using FACTOR = Factor;
    static const auto NUM {Factor::num};
    static const auto DEN {Factor::den};

    explicit Measurement(Rep value) noexcept : m_value {value}
    { /* Empty */
    }

    // C-TORS (copy & move) and assignment (copy & move) overloads for
    // Measurements<Rep, Factor> argument (defaulted with = default) and
    // template <typename Rep2, Factor2> Measurements<Rep2, Factor2> argument

    [[nodiscard]]
    constexpr auto Value() const noexcept -> Rep
    {
        return m_value;
    }

    // Can't do this, it would allow adding kilograms to meters
    /*
    template <typename Rep2, typename Factor2>
    constexpr auto operator+ (const Measurement<Rep2, Factor2>& other) const
    {
        using Adjustment              = std::ratio_divide<Factor2, Factor>;
        constexpr const double FACTOR = static_cast<double>(Adjustment::num) / static_cast<double>(Adjustment::den);

        return Measurement<Rep, Factor> {
          static_cast<Rep>(static_cast<double>(this->Value()) + (static_cast<double>(other.Value()) * FACTOR))
        };
    }

    template <typename Rep2, typename Factor2>
    constexpr auto operator+ (Measurement<Rep2, Factor2>&& other) const
    {
        using Adjustment              = std::ratio_divide<Factor2, Factor>;
        constexpr const double FACTOR = static_cast<double>(Adjustment::num) / static_cast<double>(Adjustment::den);

        return Length<Rep, Factor> {
          static_cast<Rep>(static_cast<double>(this->Value()) + (static_cast<double>(std::move(other).Value()) * FACTOR))
        };
    }
    */
};


template <typename Rep, typename Factor = std::ratio<1, 1>>
class Length final : public Measurement<Rep, Factor>
{
  public:
    Length() noexcept : Measurement<Rep, Factor>() {}
    explicit Length(Rep value) noexcept : Measurement<Rep, Factor>(value) {};

    Length(const Length<Rep, Factor>& other) noexcept : Measurement<Rep, Factor>(other) {};
    Length(Length<Rep, Factor>&& other) noexcept : Measurement<Rep, Factor>(std::move(other)) {};

    auto operator= (auto&& other) noexcept -> Length<Rep, Factor>&
    {
        Measurement<Rep, Factor>::operator= (std::forward(other));
        return *this;
    };

    template <typename Rep2, typename Factor2>
    explicit Length(const Length<Rep2, Factor2>& other) noexcept : Measurement<Rep, Factor>(other) {};

    template <typename Rep2, typename Factor2>
    explicit Length(Length<Rep2, Factor2>&& other) noexcept
            : Measurement<Rep, Factor>(std::move(other)) {};

    //NOLINTNEXTLINE (modernize-use-override)
    virtual ~Length() final = default;

    template <typename Rep2, typename Factor2>
    constexpr auto operator+ (const Length<Rep2, Factor2>& other) const
    {
        using Adjustment              = std::ratio_divide<Factor2, Factor>;
        constexpr const double FACTOR = static_cast<double>(Adjustment::num) / static_cast<double>(Adjustment::den);

        return Length<Rep, Factor> {
          static_cast<Rep>(static_cast<double>(this->Value()) + (static_cast<double>(other.Value()) * FACTOR))
        };
    }
};

template <typename Rep, typename Factor = std::ratio<1, 1>>
class Mass final : public Measurements::Measurement<Rep, Factor>
{
     // copy paste of Length
};

}    // namespace Measurements

一个完整的例子可以在 Godbolt 上找到

正如你所看到的,它非常冗长。如何保持类型安全,同时减少代码重复,尤其是运算符重载?

在最好的情况下,我只会从 Measurements 继承,不需要做任何事情,只需添加特殊的重载,例如将 Area 除以 Length 应该返回 Length。

编辑 1

我不知道我在做什么,我试了一下

template <template <IsArithmetic Rep1, IsRatio Factor1> typename Outer1, template <IsArithmetic Rep2, IsRatio Factor2> typename Outer2>
concept HasSameOuterType = requires {
    requires std::same_as<Outer1<Rep1, Factor1>, Outer2<Rep1, Factor1>>;
};

但是,它说:

使用未声明的标识符“Rep1”(clang undeclared_var_use)

只是比较,也给出了一个错误:Outer1Outer2

使用模板模板参数“Outer1”需要模板参数 (clang template_missing_args)

C 模板 类型特征 ++-概念 C ++23

评论

0赞 Elliott 11/3/2023
请参阅我之前在stackoverflow上提出的一个问题。它与 C++17 有关,但我认为也适用于 C++23 中的解决方案/限制。
0赞 FalcoGer 11/3/2023
对我来说,这开始看起来很像黑魔法。我刚刚开始研究这个模板的东西,因为概念应该使它更容易阅读、写作和理解

答:

1赞 Weijun Zhou 11/3/2023 #1

这个概念本身并不难写。您可以在要求中使用带有 lambda 的类型推导。HasSameOuterType

template <typename T, typename U>
concept HasSameOuterType = requires (T t, U u) {
    []<template <typename R, typename F> typename Outer, typename Rep1, typename Factor1, typename Rep2, typename Factor2>
    (Outer<Rep1, Factor1>&, Outer<Rep2, Factor2>&){}(t, u);
};

该概念可以进一步适应您的需求(例如,考虑引用和 cv 限定符,或要求在 lambda 中满足)。我在这里保持简单。Rep1IsArithmetic

以下测试通过:

    static_assert(Measurements::HasSameOuterType<Meter, Feet>);
    static_assert(!Measurements::HasSameOuterType<Meter, Kilogramm>);
    static_assert(Measurements::HasSameOuterType<std::pair<int, double>, std::pair<float, char>>);
    static_assert(!Measurements::HasSameOuterType<std::pair<int, double>, std::tuple<int, double>>);

演示:https://godbolt.org/z/do76oY1rc