【问题标题】:How to conditionally add a function to a class template?如何有条件地将函数添加到类模板?
【发布时间】:2017-01-02 09:37:59
【问题描述】:

我有一个矩阵类模板如下:

template<typename T, std::size_t nrows, std::size_t ncols>
class Matrix
{
    T data[nrows][ncols];
public:
    T& operator ()(std::size_t i, std::size_t j)
    {
        return data[i][j];
    }
};

我想要的是仅在编译时 nrows==ncolstrue 时为实例化定义一个 .setIdentity() 函数。当nrows==ncolsfalse 时,.setIdentity()没有定义。

我正在尝试使用enable_if idiom,但这将为所有情况定义函数。不是吗?

【问题讨论】:

  • 也许不是有条件地改变现有对象,而是有一个像make_identity&lt;typename T, std::size_t N&gt; 这样的静态函数,它返回一个Matrix&lt;T, N, N&gt;
  • @GManNickG:到目前为止最喜欢的解决方案(因为它是最简单的!)。请考虑将其添加为答案。

标签: c++ class templates c++11 template-specialization


【解决方案1】:

你可以用std::enable_if在下面的模式下做到这一点

template <std::size_t r = nrows, std::size_t c = ncols>
typename std::enable_if<r == c>::type setIdentity ()
 { /* do something */ }

一个完整的例子

#include <type_traits>


template<typename T, std::size_t nrows, std::size_t ncols>
class Matrix
{
    T data[nrows][ncols];

public:
    T& operator ()(std::size_t i, std::size_t j)
    { return data[i][j]; }

    template <std::size_t r = nrows, std::size_t c = ncols>
    typename std::enable_if<r == c>::type setIdentity ()
     { /* do something */ }
};

int main()
 {
   Matrix<int, 3, 3>  mi3;
   Matrix<int, 3, 2>  mnoi;

   mi3.setIdentity();
   // mnoi.setIdentity(); error

  return 0;
}

--- 编辑---

正如 Niall 在评论中指出的那样(关于 TemplateRex 的回答,但我的解决方案存在同样的缺陷)可以规避此解决方案以这种方式显式显示行数和列数

mi3.setIdentity<4, 4>();

(但这不是一个真正的问题(恕我直言),因为mi3 是一个方阵,setIdentity() 可以使用实际尺寸(nrowsncols))甚至是

mnoi.setIdentity<4, 4>()

(这是一个大问题(恕我直言),因为mnoi 不是方阵)。

显然有 Niall 提出的解决方案(添加 static_assert;类似

    template <std::size_t r = nrows, std::size_t c = ncols>
    typename std::enable_if<r == c>::type setIdentity ()
     {
       static_assert(r == nrows && c == ncols, "no square matrix");

       /* do something else */ 
     }

或类似的东西)但我建议在std::enable_if中添加相同的检查。

我是说

template <std::size_t r = nrows, std::size_t c = ncols>
typename std::enable_if<    (r == c)
                         && (r == nrows)
                         && (c == ncols)>::type setIdentity ()
 { /* do something */ }

【讨论】:

  • 应该提防mi3.setidentity&lt;4,4&gt;()
  • @TemplateRex - 感谢您报告问题;答案已修改。
  • 是的,守卫比 enable_if 更干净,来自 clang 的错误信息量很大,+1
  • 可以通过在模板参数前添加typename ...来解决规避问题。模板参数包在正常情况下会被推断为空,用户不能再显式提供其他参数。
  • @L.F. - 哇!您的解决方案非常优雅。并且有效。但我不明白究竟是如何工作的。我的意思是...我看到写mnoi.setIdentity&lt;3u, 3u&gt;();mnoi.setIdentity&lt;int, long, 3u, 3u&gt;(); 会产生编译错误。但是,如果参数不同,为什么不能在可变参数包之后解释参数?我的意思是……你能说一下标准的哪一部分禁止这样做吗?
【解决方案2】:

懒惰且不必要的重复方式

只需添加部分特化:

template<typename T, std::size_t N>
class Matrix<T, N, N>
{
    T data[N][N];
public:
    T& operator ()(std::size_t i, std::size_t j)
    {
        return data[i][j];
    }

    void setidentity(/*whatever params*/) { std::cout << "yay!"; }
};

Live Example

对于一般的N * M 矩阵,一般模板将被实例化,而仅对于N * N 矩阵,这种特化是更好的匹配。

缺点:所有常规代码的代码重复。可以使用基类,但实际上更容易做一些 SFINAE 魔术(下)

稍微困难但更经济的方法

您还可以通过添加隐藏模板参数 NM 来使用 SFINAE,默认为 nrowsncolssetidentity,并在条件 N == M 上添加到 enable_if

template<typename T, std::size_t nrows, std::size_t ncols>
class Matrix
{
    T data[nrows][ncols];
public:
    T& operator ()(std::size_t i, std::size_t j)
    {
        return data[i][j];
    }

    template <std::size_t N = nrows, std::size_t M = ncols, std::enable_if_t<(N == M)>* = nullptr>
    void setidentity(/*whatever params*/) { 
        static_assert(N == nrows && M == ncols, "invalid");
        std::cout << "yay!"; 
    }
};

或者,由于问题被标记为 C++11,请改用 typename std::enable_if&lt;(N == M)&gt;::type

Live Example

【讨论】:

  • 这个解决方案似乎重复了非特殊情况已经完成的所有现有工作。真的值得重复吗?
  • 这需要实现整个类两次。
  • @TemplateRex:你的第二个代码似乎没有编译甚至m22.setidentity() 行。请参阅我的链接link。我需要它与C++11 兼容,看来您使用的是 GCC 6.1.0 版本。
  • @TemplateRex:实际上,即使使用您的 C++11 新解决方案,链接 link。查看链接,它给出了error: no type named 'type' in 'struct std::enable_if&lt;false, void&gt;'
  • 我可能会在setidentity 中添加一个static_assert( N == nrows &amp;&amp; M == ncols, "invalid"); 以避免愚蠢的客户端行为: Matrix&lt;int, 3,3&gt; sq; sq.setidentity&lt;4,4&gt;();
【解决方案3】:

使用伪 CRTP 为某些东西添加模块化支持。

template<class T, std::size_t nrows, std::size_t ncols>
class Matrix;

template<class T, std::size_t size>
struct MatrixDiagonalSupport {
  auto self() { return static_cast<Matrix<T, size, size>*>(this); }
  auto self() const { return static_cast<Matrix<T, size, size> const*>(this); }
  void setIdentity() {
    for (std::size_t i = 0; i < size; ++i) {
      for (std::size_t j = 0; j < i; ++j) {
        (*self())(i,j) = {};
      }
      (*self())(i,i) = 1; // hope T supports this!
      for (std::size_t j = i+1; j < size; ++j) {
        (*self())(i,j) = {};
      }
    }
  }
};
template<class T>
struct empty_t {};
template<bool b, class T>
using maybe= std::conditional_t<b, T, empty_t<T>>;

template<typename T, std::size_t nrows, std::size_t ncols>
class Matrix: public maybe<nrows==ncols,MatrixDiagonalSupport<T, nrows>>
{
  // ...

如果我们不是对角线,我们从无继承,如果我们是对角线,则类实现集合标识。

如果正确,Matrix 的用户会神奇地从其父级获得.setIdentity()

self() 内的static_cast 最终成为零成本抽象,并允许基类访问子类。

这是伪 CRTP,因为我们实际上并没有将派生类类型传递给父类,而只是为父类重构它提供了足够的信息。

此解决方案使该方法成为实际方法,并避免任何类型的 SFINAE 欺骗。

Live example

在 C++11 中,将 conditional_t&lt;?&gt; 替换为 typename conditional&lt;?&gt;::type

template<bool b, class T>
using maybe=typename std::conditional<b, T, empty_t<T>>::type;

一切都应该编译。

【讨论】:

  • 好主意,但是empty_t 需要标记以防有多个模块支持
  • @template maybe 旨在实现无缝连接。
  • @Yakk:这种用法会被称为mixin吗? (在我看来是这样......)
  • @matt 不知道叫什么模式。对不起。即使是我一直使用的模式,我也经常把名字弄错。
  • @Yakk:非常感谢。
【解决方案4】:

任何其他答案都没有提到的基本但简单的解决方案:您可以使用std::conditional 和继承。
它遵循一个最小的工作示例:

#include<type_traits>
#include<cstddef>

struct HasSetIdentity {
    void setIdentity() { }
};

struct HasNotSetIdentity {};

template<typename T, std::size_t nrows, std::size_t ncols>
class Matrix: public std::conditional<(nrows==ncols), HasSetIdentity, HasNotSetIdentity>::type
{
    T data[nrows][ncols];
public:
    T& operator ()(std::size_t i, std::size_t j)
    {
        return data[i][j];
    }
};

int main() {
    Matrix<int, 2,2> m1;
    m1.setIdentity();
    Matrix<int, 2,3> m2;
    // Method not available
    // m2.setIdentity();
}

如果您需要所有子对象共享数据,您仍然可以将数据向下移动。
这主要取决于实际问题。

【讨论】:

  • @skypkack:非常感谢!
  • 这是我认为这里给出的最干净、最惯用的方法。
【解决方案5】:

skypjackmax66 都为这个问题提供了简单的答案。这只是一种替代方法,使用简单的继承,虽然它意味着使用方阵的子类:

template<typename T, std::size_t nrows, std::size_t ncols>
class Matrix
{
protected:
    T data[nrows][ncols];
public:
    T& operator ()(std::size_t i, std::size_t j)
    {
        return data[i][j];
    }
};

template<typename T, std::size_t N>
class SqMatrix : public Matrix <T, N, N>
{
public:
    setIdentity()
    {
        //Do whatever
    }
}

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-05-30
    • 1970-01-01
    • 2010-11-10
    • 2019-10-01
    • 2020-11-06
    • 2019-10-12
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多