可扩展的特殊多态性? (C ++ 11)

时间:2014-02-28 17:07:39

标签: templates c++11 polymorphism overloading

我需要实现一个可以在矩阵上运行的通用算法,无论其表示如何。它可以是C风格的2D数组,向量向量或任意用户类(不一定提供下标运算符)。 一个明显的解决方案可能如下所示:

template<typename M>
void func(M& mat, int cols, int rows)
{
    for(int j = 0; j < rows; ++j)
      for(int i = 0; i < cols; ++i)
         doSomething(elem(mat, i, j));
}

...用户必须提供对其矩阵类型进行操作的'elem'重载。这种方法的问题是需要在'func'之前声明这个重载,以便编译代码,而不是在模板实例化之前。有没有办法绕过这个问题,不涉及丑陋的函数签名或强迫用户编写包装类和其他样板代码?

4 个答案:

答案 0 :(得分:3)

我已经尝试过,并在第一次评论后找到了可能的解决方案。所以只需将声明包含在泛型函数中,就可以了!请参阅下面的示例代码。

最后一点似乎是 templetized 代码没有在非模板化代码中完成elem()的声明。

<击>

它是在模板的定义之后,但在它的第一次实例化之前,所以,据我所知/理解,它应该足够了......但我的编译器(gcc 4.8.2)也抱怨。我肯定已经多次使用这个功能,使用了寺庙化的方法和课程。

这对我来说似乎很奇怪,可能还有一个错误(@Potatoswatter:你可以提一下这个bug吗 - 看看这个匹配吗?)。

编辑:终于明白了。还在研究C++11 Stroustrup's!它按标准工作。我在这里给出一些指示 - 摘录。

第23.3.2段建议了第一个重要的想法(模板,错误检测):在第一个实例中使用语法错误之前,会在定义中检查语法错误。当然它们是,但尽管它只是稍后定义。但是:“模板定义中使用的名称必须在范围内或以某种合理明显的方式取决于模板参数”。这已经足够清楚了,但最重要的是这个想法背后的基本原理。

在26.3( Instantiation [of templates!],Name Binding )中详细解释:“定义模板函数以最小化对非本地信息的依赖性。原因是模板将用于基于未知类型和未知上下文生成函数和类。每个细微的上下文依赖都可能表现为某人的问题......“。

阅读之后 - 我仍然在问自己为什么我没有想到这样一个重要的区别,就通用类中存在的受控环境而言!!

解释继续(第745-758页),解决方法的机制特别在26.3.2(定义点绑定)和26.3.3( Point)中解释-of-instantation Binding ):

“当编译器看到模板定义时,它确定哪些名称是相关的(26.3.1)。如果名称是相关的,则查找其声明将被推迟到实例化时间(26.3.3)。”

“不依赖于模板参数的名称被视为不在模板中的名称;它们必须在定义”范围内(6.3.4)。

那是扔石头的。 elem()必须在模板定义中使用之前声明 - 它被视为不在模板中的名称。

我同意@Potatoswatter和其他人的意见。这可能是不太优雅的解决方案,因为它限制使用外部函数,没有函子,没有lambda。

另一方面,它解决了OP的问题(认为这是最初的解决方法......不,这只是它的工作方式!)。

#include <iostream>
using namespace std;

template<typename M, typename R>
void func(M mat, int cols, int rows)
{
  // with this declaration it works.
  R &elem(M, int, int);

  for(int i = 0; i < cols; ++i) {
    for(int j = 0; j < rows; ++j) {
      elem(mat, i, j) += 1; // +=1 is just your "doSomething()"
    }
  }
}

template<typename M, typename R>
void show(M mat, int cols, int rows)
{
  R &elem(M, int, int);
  for(int i = 0; i < cols; ++i) {
    for(int j = 0; j < rows; ++j) {
      if (j>0) cout << ", ";
      cout << elem(mat, i, j);
    }
    cout << endl;
  }
}

float &elem(float *m, int i, int j) {
  return m[i*3+j];
}

float &elem(float m[3][3], int i, int j) {
  return m[i][j];
}


int main(int argc, char **argv) {
  float mat1d[9] = {1,2,3,4,5,6,7,8,9};
  float mat2d[3][3] = {1,2,3,4,5,6,7,8,9};
  func<float*, float>(mat1d, 3, 3);
  show<float*, float>(mat1d, 3, 3);

  func<float(*)[3], float>(mat2d, 3, 3);
  show<float(*)[3], float>(mat2d, 3, 3);
}

尝试在你的问题中使用引用我有点疯狂,然后才明白将它们与静态声明的大小混合,让事情更多卡住。我把它包括在这里是因为我已经失去了一些时间来试图解决这个问题:

#include <iostream>
using namespace std;

template<typename M, typename R>
void func(M &mat, int cols, int rows)
{
  R &elem(M&, int, int);
  for(int i = 0; i < cols; ++i) {
    for(int j = 0; j < rows; ++j) {
      elem(mat, i, j) += 1; // +=1 is just your "something"
    }
  }
}

template<typename M, typename R>
void show(M &mat, int cols, int rows)
{
  R &elem(M&, int, int);
  for(int i = 0; i < cols; ++i) {
    for(int j = 0; j < rows; ++j) {
      if (j>0) cout << ", ";
      cout << elem(mat, i, j);
    }
    cout << endl;
  }
}

float &elem(float (&m)[9], int i, int j) {
  return m[i*3+j];
}

float &elem(float (&m)[3][3], int i, int j) {
  return m[i][j];
}


int main(int argc, char **argv) {
  float mat1d[9] = {1,2,3,4,5,6,7,8,9};
  float mat2d[3][3] = {1,2,3,4,5,6,7,8,9};
  func<float[9], float>(mat1d, 3, 3);
  show<float[9], float>(mat1d, 3, 3);

  func<float[3][3], float>(mat2d, 3, 3);
  show<float[3][3], float>(mat2d, 3, 3);
}

注意:这样elem()是一个函数,包含在链接时。我认为这不是你想要的,但是你可以绕过它来制作所有东西的算子。

答案 1 :(得分:2)

最简单的解决方案是下标运算符应该是必需的。

您可以强制用户声明elem函数,但您不想强迫他重载下标运算符。这对我来说没有意义。考虑他使用已经定义了下标运算符的数组或类的情况。为什么要强迫他定义一个执行下标操作符已经完成的功能?

然而,这就是你想要的方式:

template<typename M, typename F>
void func(M& mat, int cols, int rows, F elem)
{
    for(int j = 0; j < rows; ++j)
      for(int i = 0; i < cols; ++i)
         doSomething(elem(mat, i, j));
}

可以通过引用函数,指向函数或lambda的指针来调用它。

使用lambda调用示例:

func(mat, 5, 5, []->int (int **m, int i, int j) { return m[i][j];});

我没有测试过,所以我希望没有语法错误。

作为一个中间地带,你可能会有一个重载,不接收elem作为参数并使用下标运算符。

答案 2 :(得分:2)

  

这种方法的问题是这个重载需要在'func'之前声明,以便编译代码,而不是在模板实例化之前。

这听起来像是一种误解;它比实际要求强得多。由特定专业化调用的重载只需要在该专业化的实例化之前声明,即在每个首先导致模板的翻译单元中的非模板调用之前声明。

在模板定义之前,您只需要一些名为func函数;它不需要远程匹配呼叫或用于任何目的。它只是一个占位符,让解析器知道你的模板在那里进行函数调用。这样做会很好:

void elem( struct unused_tag_type ) = delete;

就像这样:

void elem();

答案 3 :(得分:0)

以下是我尝试使用最小运行样本来演示问题和解决方案。当函数和数据在不同的命名空间中定义时,我只能产生问题。我认为这是最常见的情况。

问题live example):

namespace X { struct A {}; }

namespace Y { // function-style

    template<typename T>
    void f(T x) { h(x); }

    void g() { f(X::A{}); }

    void h(X::A) { cout << "Hello, world!" << endl; }

}

解决方案live example):

namespace X { struct A {}; }

namespace Y { // functor-style

    template<typename T>
    struct H;

    template <typename T>
    void h(T x) { H<T>()(x); }

    template<typename T>
    void f(T x) { h(x); }

    void g() { f(X::A{}); }

    template<>
    struct H<X::A>
    {
        void operator()(X::A) { cout << "Hello, world!" << endl; }
    };

}

简而言之,使用模板结构(函数对象,H)来完成工作,使用模板函数(h)仅作为包装器。 H可以随时使用(但在同一名称空间中)。它的一般声明和h声明应出现在g()的实例化之前。

限制live example):不幸的是,如果返回类型未知,也就是说,如果您将Hh替换为最一般的形式

template<typename... T>
struct H;

template <typename... T>
auto h(T&&... x)
->decltype(H<T...>()(std::forward <T>(x)...))
  { return H<T...>()(std::forward <T>(x)...); }

我已经尝试过,如果没有训练返回类型,它甚至不能与-std=c++1y一起使用。但我希望这不是你的问题。