为什么不像std :: vector这样的标准容器实现接口?

时间:2020-04-20 19:06:05

标签: c++

我想存储一个队列向量。

队列可能具有不同的模板类型,因此我无法将它们存储在同一向量中。

LOGGING = {
    'version': 1,
    'disable_existing_loggers': False,
    'formatters': {
        'simple': {
            'format': '{asctime} {name}] {message}',
            'style': '{',
        }
    },
    'handlers': {
        'file': {
            'level': 'ERROR',
            'class': 'logging.FileHandler',
            'filename': 'error.log',
            'formatter': 'simple',
        },
    },
    'loggers': {
        'django': {
            'handlers': ['file'],
            'level': 'ERROR',
            'propagate': True,
        },
        '': {
            'handlers': ['file'],
            'level': 'ERROR',
            'propagate': False,
        },
    },
}

我可以在向量中存储指向队列的指针,但是队列并不都实现相同的基类(例如IQueue)。

std::queue<int> aQueue1
std::queue<std::string> aQueue2
std::queue<float> aQueue3
std::vector<std::queue> aVec // Doesn't work because std::queue needs template arguments

为什么不存在? 我有什么选择?

在我的情况下,用法是我要检查所有队列是否为空。 std::vector<std::IQueue*> aVec //IQueue does not exist

5 个答案:

答案 0 :(得分:7)

您在此处使用的std库的一部分– std::queue –来自称为“标准模板库”的库。

在其中,Alexander Stepanov得出结论,您可以将容器操作与要操作的数据脱钩,并将算法与它们所操作的容器脱钩。所有这些都在生成接近于手工C语言性能的代码。

这太神奇了。

您可以使用代码生成实用程序执行类似的操作,但是编译器或调试器无法理解这样生成的代码,至少是如何将其连接回原始代码。 STL使每个C ++用户都可以使用性能经过调整且无错误的实现的红黑树,而无需考虑太多。

现在,C ++的原则之一就是您不用为不使用的东西付费。在向对象添加虚拟接口时,会增加一堆开销。

首先,每个实现类的虚拟功能表必须在运行时存在。第二,添加RTTI。第三,内联机会消失了。第四,实现对象必须带有一个额外的vtable指针。第五,调度到方法需要额外的间接层。第六,像迭代器这样的连锁类型变得越来越复杂。

所有这些费用都不由std::queue<int>承担。如果您std::queue<int>的实现大致是基于块分配的双数组表编写自己的自定义C队列所得到的。

我的意思是,如果您想要一个队列,您可能不会写。但是,如果您有很多时间,可能会。


其他语言选择了不同的路径。诸如Java和C#之类的语言几乎所有对象都通过继承和虚拟表进行堆分配。对象实际上是对对象的垃圾回收引用,并且内存局部性几乎是不可能的。

在大多数任务中,这样做的成本大约将性能降低了2到3倍。他们可以在狭窄的情况下使用外部库来解决此问题,或者通过非常仔细地编写代码来使他们的优化器消除所有对象开销。

对于许多人来说,性能降低2到3倍是可以的。毕竟,对性能的投资是可以替代的,并且在Java和C#中,许多任务要容易得多。因此,您可以编写一个更快的应用程序,使其在C#/ Java中陷入困境,然后将工作重点放在关键路径上。将汗水投入到代码中时,将代码的性能提高一倍是非常典型的。

C ++并不是真正的OO语言。它具有编写C ++的功能,面向对象和过程方式。

关于如何解决?您编写自己的 own 队列抽象。我脑海中浮现出四种抽象队列的方法。

  1. 您可以编写自己的基本队列类。具有自己的基本队列值。

  2. 您可以编写一个std::variant个队列或一个std::variant个队列。

  3. 您可以写一个std::any的队列。

  4. 您可以键入擦除队列操作并构建自己的vtable实现以允许多态值类型。

哪个是最好的?这将取决于您的代码编写能力,问题空间的详细信息以及所使用的C ++版本。

答案 1 :(得分:6)

这根本不是C ++的设计方式。如果您想要std::queue<std::queue< A few similar types >>,则可以使用std::queue<std::queue<std::variant< types >>>。如果希望它包含各种不同类型的开放式列表,则可以使用std::queue<std::queue<std::any>>。如果所有类型都共享一个公共基数,则可以使用指向基类的(智能)指针队列。如果您有更多冒险的想法,C ++仍然可以满足这些需求。我不建议这样做,但是您甚至可以将void*元素存储在队列中,并自己进行类型簿记。

话虽如此,即使有可能,但经常(但可能并非总是)有一个更简单的解决方案供您使用。

答案 2 :(得分:2)

在某个时间点,决定这些容器将使用泛型编程范例而不是诸如基本类和多态的面向对象编程范例的权力继承。

这个决策过程将权衡各种利弊。两者都有很多,超出了此处列出的范围。

但是,正如您所发现的,缺点之一是您不能直接执行您所描述的事情。但是,这似乎是一个极端的要求,不值得所有虚拟分派的运行时开销。需要将各种“某些事物的队列”视为一堆事物是非常罕见的。而且,在C ++中,您不用为不使用的东西付费。

您可以通过其他方式获得想要的东西。我会这样:

DECLARE @Temp TABLE (
[closed_at] smalldatetime,
[number] varchar(40),
[opened_at] smalldatetime,
[order] int
)

DECLARE @SQLString nvarchar(max);

SET @SQLString = 'SELECT * FROM OPENQUERY(SERVICENOW, ''SELECT 
closed_at,
number,
opened_at,
"order"
FROM incident
WHERE opened_at BETWEEN ''''2019-01-01 00:00:00'''' and ''''2019-02-01 23:59:59''''; '')';

INSERT @Temp ( 
closed_at,
number,
opened_at,
"order"
)

EXECUTE(@SQLString);


UPDATE dbo.inc
SET
closed_at = cte.closed_at,
number = cte.number,
opened_at = cte.opened_at,
"order" = cte."order"
FROM
dbo.inc inc
INNER JOIN @Temp cte on cte.number = inc.number
WHERE isnull(cte.sys_updated_on,'') <> ISNULL(inc.sys_updated_on,'')

(实际上,将变体包装到对您的“业务逻辑”建模的某种类类型中)

请注意,您不再需要将指针存储在外部队列中,因此这已经是您已经消灭的一种间接级别(和不必要的动态分配)。

不利之处在于,您必须在using QueuesType = std::variant<std::queue<int>, std::queue<std::string>, std::queue<float>>; std::queue<QueuesType> aVec; 的定义中指定所需的每种queue类型,或切换到QueuesType。此外,变体的用法中还需要一些机制。

但是您生成的代码可以相当优雅并且可重复使用。

答案 3 :(得分:2)

如果需要,可以使用多态性:

struct my_interface {
    // put methods here
};

struct int_impl : my_interface {
    std::queue<int> data;
};

struct string_impl : my_interface {
    std::queue<std::string> data;
};

然后使用

std::queue<std::unique_ptr<my_interface>>

如果这就是您想要的,那么C ++不会阻止您这样做。但是,将运行时多态性的成本强加给每个人都会使那些不需要它的人不满意。 C ++的座右铭是“不要为不使用的东西付钱”,因此java的做事方式(所有内容均继承自Object)是不可行的。 C ++可以让您自由地为要放入std::queue<std::unique_ptr<my_interface>>的对象定义接口,但这不会迫使您实现一些特殊的接口来将某些东西放入队列。

请注意,使用接口是侵入性的:您不能将int放入需要特定接口的队列中,而无需将其包装在除了实现接口之外没有其他用途的类中。标准容器比这更通用。您可以将符合标准的任何物品放入标准容器中。将某些东西放到容器中永远不需要您修改类型(例如,使其从某个接口继承)。

PS:empty是一个简单的示例,但请考虑像frontback这样的方法,它们返回对队列中元素的引用。他们应该返回什么?没有明显的答案,普通的运行时多态性也没有答案。有解决方案,但没有一种适合所有解决方案。有许多设计决策,因此C ++为您提供了许多工具来实现您的目标(例如std::variant),而不是提供一种适合约1%用例的即用型解决方案,大约5%可以,但是其余没有用。

答案 4 :(得分:0)

您可以使用多态来实现此行为:

class EmptyBase{
public:
    virtual ~EmptyBase() = default;
    virtual bool empty() const = 0;
};


template<class Queue>
class EmptyBaseTemplate : public EmptyBase{
    Queue* q_;
public:
    bool empty() const override{
        return q_->empty();
    }
    explicit EmptyBaseTemplate(Queue& q) : EmptyBase(), q_(&q){};
};

int main() {
    std::queue<int> aQueue1;
    std::queue<std::string> aQueue2;
    std::queue<float> aQueue3;
    std::vector<EmptyBase*> aVec;
    aVec.push_back(new EmptyBaseTemplate(aQueue1));
    aVec.push_back(new EmptyBaseTemplate(aQueue2));
    aVec.push_back(new EmptyBaseTemplate(aQueue3));
    for(auto& el : aVec)
        std::cout<<el->empty()<<std::endl;
}

记住,这绝对不是您应该使用的代码,仅用于演示目的

注意:这项工作仅适用于一小部分“前提”,如果您想做更高级的工作,则variant-{{1}一定不能涉及参数和返回类型的不同类型。 }模式是一种更容易且可能更好的方法

相关问题