【问题标题】:Identical template for many functions许多功能的相同模板
【发布时间】:2014-01-03 01:48:59
【问题描述】:

大家好(新年快乐!)

我正在用 C++ 编写一个(不是真的)简单项目(我的第一个项目,来自纯 C)。我想知道是否有一种方法可以简化具有相同模板模式的多个函数的定义。我认为一个例子会更好地解释这个问题。

上下文

假设我有一个代表数字列表的“Set”类,定义为

template <class T>
class Set {
    static_assert(std::is_arithmetic<T>(), "Template argument must be an arithmetic type.");
    T *_address;
    ...
}

所以如果我有一个实例(比如Set&lt;double&gt;)和一个数组U array[N],其中U 是另一种算术类型,N 是一个整数,我希望能够执行一些操作,例如将数组的值分配给Set 的值。因此我在类中创建了函数模板

template <class U, int N>
void assign(U (&s)[N]) {
    static_assert(std::is_arithmetic<U>(), "Template argument must be an arithmetic type.");
    errlog(E_BAD_ARRAY_SIZE, N == _size);
    idx_t i = 0;
    do {
        _address[i] = value[i];
    } while (++i < size);
}

问题

就我的测试而言,上面的代码运行良好。但是我发现它真的很难看,因为我需要static_assert 来确保只将算术类型作为参数(参数U)并且我需要一种方法来确定数组大小(参数N) .另外,我还没有完成assign 函数,但我需要很多其他函数,例如addmultiplyscalar_product 等等!

第一种解决方案

那时我想知道是否有更漂亮的方法来编写这种类。经过一些工作,我想出了一个预处理器指令:

#define arithmetic_v(_U_, _N_, _DECL_, ...)                                             \
        template <class U, idx_t N> _DECL_                                              \  
        {                                                                               \
            static_assert(std::is_arithmetic<U>(),"Rvalue is not an arithmetic type."); \
            errlog(E_BAD_ARRAY_SIZE, N == _size);                                       \
            __VA_ARGS__                                                                 \
        }

因此将我的函数定义为

arithmetic_v(U, N,
             void assign(U (&value)[N]),
                 idx_t i = 0;
                 do {
                     _address[i] = value[i];
                 } while (++i < _size);
             )

这在某种程度上更干净,但仍然不是最好的,因为我不得不丢失包裹函数主体的括号(必须在函数本身中包含 static_assert 以使模板参数 U 在范围内)。

问题

我发现的解决方案似乎工作得很好,代码比以前更具可读性,但是......我不能使用另一个构造来构建所有函数的更清晰的定义并且仍然保留static_assert 和有关数组大小的信息?为我需要的每个功能重复一次模板代码真的很难看...

谢谢

我只是想了解该语言,因此任何有关此论点的其他信息将不胜感激。我已经尽可能多地搜索,但找不到任何东西(也许我只是想不出合适的关键字来询问谷歌以找到相关的东西)。提前感谢您的帮助,祝大家新年快乐

詹卢卡

【问题讨论】:

  • 这个问题更适合CodeReview
  • #define big no no :E​​span>
  • 为什么要在运行时测试大小?为什么你需要测试U
  • 为什么要定义自己的?使用std::set
  • @Yakk 的大小并不总是在运行时进行测试(只有在编译时定义了某些单词)。 errlog 是一个预处理器宏,在这种情况下包含一种static_assert,但在某些情况下,作为参数传递的数组只能在运行时构建。我需要测试U,因为在双精度和字符串之间求和并不是我的想法。我根本不想创建字符串集!如果您知道任何更好的方法来测试模板参数是否属于算术类型,我会很乐意倾听。

标签: c++ templates preprocessor-directive


【解决方案1】:

我强烈建议不要使用宏,除非没有办法绕过它们(我能想到的一种情况是获取行号以进行调试)。来自 Google C++ 风格指南 (http://google-styleguide.googlecode.com/svn/trunk/cppguide.xml#Preprocessor_Macros):

宏意味着你看到的代码和编译器看到的代码不一样。这可能会引入意外行为,尤其是因为宏具有全局范围。

我真的不明白你为什么考虑使用丑陋的static_assert。还有另一种方法可以确保模板仅适用于使用 SFINAE 的某些类型。

template <class T, class Enable = void>
class X;

template <class T>
class X<T, typename std::enable_if<std::is_integral<T>::value>::type> {
};

您可以使用using 语句(没有双关语)来做这个更漂亮:

template <class T>
using enable_if_integral_t = typename std::enable_if<std::is_integral<T>::value>::type;

template <class T, class Enable = void>
class X;

template <class T>
class X<T, enable_if_integral_t<T>> {
};

现在

X<int> x; // ok, int is integral
X<float> y; // compile error

SFINAE(替换失败不是错误)是 C++ 中的一项功能,如果模板特化失败,您不会收到错误消息。

template &lt;bool Cond, class T = void&gt; struct enable_if。如果Cond 为真,则类型T 被启用为成员类型enable_if::type。否则,enable_if::type 未定义。所以对于浮点类型 is_integral 是 false 并且 enable_if::type 不存在,所以模板特化

template <class T>
class X<T, typename std::enable_if<std::is_integral<T>::value>::type>

失败,但使用通用模板代替

template <class T, class Enable = void>
class X;

已声明但未定义。

这很有用,因为您可以拥有更多专业化,例如:

template <class T>
using enable_if_integral_t = typename std::enable_if<std::is_integral<T>::value>::type;

template <class T>
using enable_if_floating_t = typename std::enable_if<std::is_floating_point<T>::value>::type;


template <class T, class Enable = void>
class X;

template <class T>
class X<T, enable_if_integral_t<T>> {
};
template <class T>
class X<T, enable_if_floating_t<T>> {
};

希望你觉得这至少很有趣。

新年快乐!

编辑

我应该将&lt;T, enable_if_integral_t&lt;T&gt;&gt; 放在函数定义中的什么位置?我只能用类模板来完成这个...

对于函数, enable_if::type 可以是返回类型。例如,如果 f 返回int,您可以:

#include <type_traits>

template <class T>
typename std::enable_if<std::is_integral<T>::value, int>::type f(T a) {
    return 2 * a;
}

int main() {
    f(3); // OK
    f(3.4); // error
    return 0;
}

using:

#include <type_traits>

template <class T, class Return = void>
using enable_if_integral_t = typename std::enable_if<std::is_integral<T>::value, Return>::type;

template <class T>
enable_if_integral_t<T, int> f(T a) {
    return 2 * a;
}

int main() {

    f(3); // OK
    f(3.4); // Error
    return 0;
}

【讨论】:

  • 谢谢!在转向我发布的方法之前,我确实尝试过这种方法,但我从未设法让它发挥作用。但是我的代码有点不同,所以我会再试一次,让你知道!
  • 试过但无法正常工作!我应该将&lt;T, enable_if_integral_t&lt;T&gt;&gt; 放在 function 定义中的什么位置?我只能用类模板来完成这个......
【解决方案2】:

我不明白您为什么认为 static_assert 或 errlog 语句如此丑陋,并怀疑部分原因是对语言不熟悉。不过,您可以轻松地编写一个函数或宏(如果您想在 assign 等函数中使用 __LINE__),将它们移出一行,允许使用如下:

template <class U, int N>
void assign(U (&s)[N]) {
    assert_array_n_numbers(s);
    idx_t i = 0;
    do {
        _address[i] = s[i];
    } while (++i < size);
}

我不能使用另一个构造来构建所有函数的更清晰的定义,并且仍然保留 static_assert 片段和有关数组大小的信息吗?为我需要的每个功能重复一次模板代码真的很难看...

可能而言 - 尽管恕我直言,这可能是不受欢迎的混淆 - 您可以让您的函数接受具有来自数组的模板化隐式构造函数的(一个)参数,验证它的算术构造函数然后使用它验证函数中的大小,允许使用如下:

template <typename U>
void assign(Arithmetic_Array<U>& s) {
    assert_same_size(s);
    idx_t i = 0;
    do {
        _address[i] = s[i];
    } while (++i < size);
}

实施:

template <typename T>
class Arithmetic_Array
{
  public:
    template <size_t N>
    Arithmetic_Array(T (&a)[N])
      : p_(&a), size_(N)
    {
        static_assert(std::is_arithmetic<T>(),"Rvalue is not an arithmetic type.");
    }

    T& operator[](size_t i) { return p_[i]; }
    const T& operator[](size_t i) const { return p_[i]; }

    size_t size() const { return size_; }

  private:
    T* p_;
    size_t size_;
};

讨论

“更干净”可以是主观的。特别是,您应该考虑使用直观类型的 C++ 源代码作为文档和可维护性的“正常”非宏的价值。如果宏大大简化了许多功能——特别是如果它只用于实现文件而不是共享头文件——那么它是值得的,但如果只有边际收益,那么混淆和去本地化就不值得了.当您刚接触该语言时,所有这些模板的内容可能看起来令人费解且难看,但过一段时间后,它就会一目了然,并帮助读者理解该函数的作用。

在 C++ 中,对模板的参数多态性采取“鸭子类型”的态度也很常见。这意味着您可以让人们传入任何类型的参数,并且如果这些类型支持模板实现尝试对它们进行的操作(即编译),那么希望这将是调用者想要的。这就是创建具有可预测语义行为的类型是一个好主意的原因之一,例如,仅当影响类似于内置类型或std::string 上的相同运算符时才使用运算符重载。

不过,您想要的更严格的执行也有它的位置 - Bjarne Stroustrup 和其他人花费了大量时间研究“概念”,这是一种对用作模板参数的类型强制执行期望的机制,并且会是一个不错的选择适合您的“算术类型”规定。我希望他们能成为下一个 C++ 标准。同时,静态断言是一个不错的选择。

【讨论】:

  • 感谢您的回答!然而,创建这样一个函数的全部意义在于能够将标准 C 数组分配给我的 Set 内部数组,因此创建另一个包装类将无法完成这项工作......我需要能够做类似 @987654327 的事情@ 后跟s1 = (double[3]) {1, 2, 3}; 你的方法有可能吗?
  • but after a while it's understood at a glance and helps readers understand what the function goes on to do. 知道了。我正在删除宏并将所有内容都内联,您的陈述很有意义。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2023-03-20
  • 1970-01-01
  • 2019-06-21
  • 1970-01-01
  • 2022-01-01
  • 1970-01-01
相关资源
最近更新 更多