【问题标题】:Data structure with type-specific, but without template具有特定类型但没有模板的数据结构
【发布时间】:2016-07-20 14:14:22
【问题描述】:

我注意到大多数(全部?)可以存储用户定义类型的 C++ 数据结构都使用模板:

std::vector<T>, std::unordered_map<T....>, etc.

在我看来这是必需的,否则这些数据结构将不得不返回为用户必须手动转换的险恶指针void*,如下所示:

std::vector a; 
int b;
a.push_back(b);
static_cast<int>(a.get(0));

可以不使用模板吗?还有其他替代方法吗?

编辑: 许多 cmets 建议没有模板,这是不可能/不切实际的。 (谢谢!)

如果我将 &lt;T&gt; 限制为真正的指针(例如,不是 int/float),它仍然不切实际吗?

【问题讨论】:

  • static_cast on get 只是问题的一小部分。在您的示例中,假设的 vector::push 的声明是什么?
  • 有可能,但这意味着采用纯 C 风格的策略。我可以给你的主要例子是链表和 RB 树在 Linux 内核中的实现方式
  • 使用模板,您可以获得按值存储元素的容器。在void * 的情况下,这不是一个选项...
  • 您觉得必须避免使用的模板是什么?它们是解决这个问题的绝佳方法。
  • 您认为模板阻止您解决的实际问题是什么?你需要一个异构的还是什么?

标签: c++ templates data-structures


【解决方案1】:

可以不使用模板吗?

是的。可以定义一个通用的数据结构,它允许对 任何类型的对象进行分组,而无需使用模板。

有“C 方式”(C 中不存在模板)。 C方式是使用void*。您显然已经很熟悉了,但是对于答案的其他读者来说,void* 是一种特殊的指针类型,它可以指向任何类型的对象。当您使用 void* 来引用一个对象时,您实际上抛弃了该语言提供的所有类型安全性。

我认为标准库中没有任何非模板容器。但是,如果使用提供的模板来实例化std::vector&lt;void*&gt; 或类似的不是太多,那么您当然可以使用这样的模板实例来存储指向任何对象的指针。

模板比void* 更受欢迎,因为模板不会将类型安全抛到窗外。模板更容易正确使用。

还没有。但std::any 计划在 C++17 中引入。它可以用来代替void*。使用std::any 你仍然抛弃了编译时类型安全性,但至少你保留了运行时安全性(当你有错误时你会得到一个异常而不是潜在的龙)。与void* 不同,std::any 管理存储对象的内存。请注意,虽然std::any 不是模板,但它的大部分成员函数都是。


还有其他替代方法吗?

除了模板和void*?从技术上讲,您还可以使用宏来生成同一程序的不同版本,类似于实例化模板。这有时在 C 中使用。在 C++ 中没有理由这样做,因为模板在各方面都更好。

【讨论】:

  • 嗯分组 -> 存储一个集合?
  • @TimStraubinger 对于确实存储对象的容器(如标准容器)来说,这是一个合适的词,但我们也在考虑void*。当我们使用void* 时,我们不是在“存储” 对象。我们只是参考他们。这就是为什么我很难想出一个词来描述一般数据结构的使用。
【解决方案2】:

我会给你一个例子,为什么使用这样的集合会很危险。考虑下面的代码:

#include <vector>
#include <iostream>

using anyvector = std::vector<void *>;

int main() {
   anyvector av;
   av.push_back(new int(10));
   std::cout << (*static_cast<int*>(av[0])) << std::endl;
}

av[0] 是一个指向 int 的指针,它由老式 c++ 方式使用 new 关键字创建。因此,应该使用delete 关键字释放以这种方式分配的内存。但是要做到这一点,应该首先将指针转换为正确的类型,让编译器知道他真正删除了什么,以防止出现未定义的行为。

要清楚,我并不是说它不可能也不切实际,但它需要程序员全神贯注......

【讨论】:

  • 这就是为什么像boost::any,很快就会成为std::any,是带有额外脚手架的模板来跟踪类型,调用正确的deleter,等等。手动执行此操作真的没有'正如您所指出的,这似乎不值得。根据我对“现代 C++”的理解,任何需要手动内存管理、外来类型转换等的东西都应该由库或其他精心制作的包装器来处理;用户代码不应该做任何事情。
  • 有人需要编写库和包装器。用户代码(无论这意味着什么)也可以是一个库。
【解决方案3】:

您可以依赖类型擦除结构。
作为一个最小的、有效的和幼稚的例子:

#include<memory>
#include<iostream>
#include<vector>

struct Container {
    struct B {
        virtual void operator()() = 0;
    };

    template<class T>
    struct D: B {
        D(T t): o{t} {}
        void operator()() override { o(); }
        T o;
    };

    template<typename T>
    void add(T t) {
        vec.push_back(std::unique_ptr<B>{new D<T>{t}});
    }

    void operator()() {
        for(auto &ref: vec) (*ref)();
    }

    std::vector<std::unique_ptr<B>> vec;
};

struct P {
    void operator()() { std::cout << "P" << std::endl; }
};

struct Q {
    void operator()() { std::cout << "Q" << std::endl; }
};

int main() {
    Container c;
    P p;
    Q q;
    c.add(p);
    c.add(q);
    c();
}

C++17 可能会引入std::any,让你轻松搞定。
在此之前,您可以使用boost::any 或类似上述的自制类型擦除类。

【讨论】:

    【解决方案4】:

    我同意模板是处理容器的正确方法,std::any(或boost::any)当您不能使用容器时。然而,只是为了有趣的部分,我想指出完美的类型重建是可以通过异常进行的。请注意,这是非常不推荐,我只是为了完整性而添加它。在这种情况下,

    • 你的向量就像std::vector&lt;std::function&lt;void()&gt;&gt;(可以简化),
    • 你推送 v.push_back([e](){ throw e; });
    • 之类的元素
    • 您访问元素的方式不亚于catching 引发的异常,例如try{ v[i](); } catch(EType&amp; e) { ... }

    这很难看,不要这样做,这很慢,不要这样做.. 但是,另一方面,它允许完美的访问(包括继承,这是 type_info 所没有的) )、开放的层次结构(variant 没有)、链接访问者,无需基类和额外的虚函数。

    BIG FAT WARNING:你可能不会用这样的代码通过我的 CR - 只是为了有趣和理论上的完整性而提到它。

    【讨论】:

      猜你喜欢
      • 2014-09-16
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2011-04-02
      • 1970-01-01
      • 1970-01-01
      • 2012-12-26
      • 1970-01-01
      相关资源
      最近更新 更多