【问题标题】:Extensible ad hoc polymorphism? (C++11)可扩展的临时多态性? (C++11)
【发布时间】:2014-02-28 17:07:39
【问题描述】:

我需要实现一个通用算法,该算法可以对矩阵进行运算,而不管其表示形式如何。它可以是 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'之前声明才能编译代码,而不仅仅是在模板实例化之前。有没有办法解决这个问题,不涉及丑陋的函数签名或强制用户编写包装类和其他样板代码?

【问题讨论】:

  • 我不认为“这个重载需要在'func'之前声明才能编译代码”:代码只会在你第一次实例化模板时由编译器生成.在模板函数定义中调用 elem() 是完全合法的。重要的是在第一次使用 func() 之前定义 elem(T&, int, int)。
  • 这不是我的编译器 (GCC 4.7) 所说的,不幸的是...“[Error] 'at' 没有在这个范围内声明,并且此时依赖于参数的查找没有找到任何声明实例化的 [-fpermissive] [注意] 'float& at(float (*)[3], int, int)' 在这里声明,稍后在翻译单元中”
  • @user3026691:在这种情况下,您使用的是 funcfloat(*)[3]。正如他所说,elem 必须在func使用 之前定义。它可以在func定义之前或之后定义。

标签: templates c++11 polymorphism overloading


【解决方案1】:

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

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

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

这对我来说似乎真的很奇怪,可能是一个错误(@Potatoswatter:你能给出这个错误的参考 - 看看是否匹配?)。

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

第一个重要的想法是在 23.3.2 中提出的(模板,错误检测):在第一次使用定义之前,先检查定义中的语法错误。当然是,但它只是后来才定义的。但是:“模板定义中使用的名称必须在范围内或以某种合理明显的方式取决于模板参数”。这一点现在已经很清楚了,但最重要的是这个想法背后的基本原理。

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

读完之后——我仍然在问自己,为什么我没有想到关于泛型类中存在的受控环境的如此重要的区别!

继续解释(第 745-758 页!),解决机制在 26.3.2(Point-of-definition Binding)和 26.3.3(Point实例化绑定):

“当编译器看到模板定义时,它会确定哪些名称是依赖的(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() 是一个函数,包含在链接时。我认为这不是您想要的,但是您可以绕过它,制作所有东西的函子。

【讨论】:

  • 这就是诀窍!但是,编译器无法推断出 R 的类型,所以我不得不切换 M 和 R 的位置,让它至少推断出矩阵类型。谢谢! =)
  • 此解决方案的潜在问题是elem 不能是模板,而必须是具有精确签名R &amp;elem(M&amp;, int, int) 的一组非重载函数。未找到 elem 的可能原因是 ADL 不起作用,例如,如果 Mint [3][3]。非 ADL 重载确实需要在模板之前声明,否则将被忽略。
  • @Potatoswatter 非重载是什么意思?上例中的elem函数是不是重载了?至于其他限制,对我来说似乎是合理的,因为我们在实现接口的虚拟功能时面临同样的限制。根据 bolov 的建议,您认为将 elem 作为参数传递会更好吗?
  • @user3026691 我想不出我的意思,看起来像是脑放屁或错字。对不起。不允许使用模板和转换。虚函数确实允许转换。我仍然认为我的解决方案,这几乎是你尝试的第一件事,是最合理的设计。我没有意识到你已经定义了 elem 来使用除了类之外的 C 样式数组类型,但我不明白为什么你不能在 func 之前声明特定的重载。
  • 如果用户的矩阵是向量的向量怎么办?一个 std::array 的 std::arrays?自定义数组的自定义数组?以上任何一种的任意组合? =P 在这种特殊情况下可能有点牵强,但我最初的问题中暗示了这一要求。我想这两种解决方案都有它们的用例,我有一些关于 ADL 的阅读要做!
【解决方案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));
}

这可以通过对函数的引用、指向函数的指针或 lambdas 来调用。

使用 lambda 调用示例:

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

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

作为中间立场,您可能会遇到一个重载,它不会接收elem 作为参数并使用下标运算符。

【讨论】:

  • 如果您认为没有用户类可以重载的“[][]”运算符,这将是有意义的。他们必须实现一个下标运算符,该运算符返回另一个对象,该对象带有一个返回值的下标运算符。此外,矩阵可以是第三方类型,其接口不能被修改。另外,我希望避免将 elem 作为参数传递。
  • 是的,这实际上是在实践中完成的。最好的办法是有 2 个重载:没有自定义函数,使用双下标;并带有自定义功能。
  • @bolov:如果matint*,那么[][] 将不起作用,但elem 会。
  • 如果您正在寻找其他示例,例如 elem() 以查看它是否有用,请考虑 std::begin()std::end()
【解决方案3】:

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

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

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

void elem( struct unused_tag_type ) = delete;

就像这样:

void elem();

【讨论】:

  • GCC 4.7 抱怨使用已删除的函数。删除删除位,我收到有关使用不完整类型的错误。也许我做错了什么?您能否提供一个可以编译的最小示例?
  • @user3026691 这是一个 GCC 错误(我报告过),不完整类型是另一个自 4.7 以来已修复的错误。将其更改为引用、指针、任何可编译的虚拟对象,或者只是空的()。我的意图不是要具体,而是要证明声明的破坏程度。我不是故意用完全不相关的问题来破坏你的编译器。
  • "只需要在该专业化的实例化点之前声明" 这也不太准确。它需要在模板定义之前声明,或者需要通过参数依赖查找找到,这意味着它必须在与参数类型关联的命名空间中声明(在实例化点之前的任何时间点)。
  • @Potatoswatter:我明白你在说什么,但无论我尝试什么,我都无法编译它。编译器的行为就好像有效的“elem”定义不存在,并且总是尝试使用损坏的声明。现在在 GCC 4.8.1 上。
  • @user3026691 ADL 可能工作不正常。我需要一个可重复的例子来进一步诊断。
【解决方案4】:

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

问题 (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 一起使用。但我希望这不是你的问题。

【讨论】:

  • 我认为您的解决方案根据 [temp.expl.spec]/6 调用未定义行为。 (你没读过“隐藏”的打油诗吗?)
  • @dyp 你能详细说明一下吗?
  • 我认为它确实关心你在哪里放置该专业。 AFAIK,[temp.expl.spec]/6 要求将专业化放在“在第一次使用会导致隐式实例化的专业化之前”,即在g之前。
  • 嗯,这很有趣,因为我很确定我已经编写了大量这样的代码,尽管我不确定实例化点可能非常晚。
  • :) 是的,甚至有几个。 [temp.point] 描述了这一点;对于g中引用的f的特化,应该紧跟在g的定义之后;另一个在翻译单元的末尾。 Here's an example from the Standard of the ill-formedness of similar things
猜你喜欢
  • 1970-01-01
  • 2019-09-28
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2019-12-18
  • 1970-01-01
  • 2010-12-05
  • 2015-08-18
相关资源
最近更新 更多