是否有任何理由不扩展 std::set 以添加下标运算符?

Posted

技术标签:

【中文标题】是否有任何理由不扩展 std::set 以添加下标运算符?【英文标题】:Are there any reasons not to extend std::set to add a subscript operator? 【发布时间】:2018-09-09 05:58:56 【问题描述】:

我使用std::set 来存储一个类的唯一实例。 std::set 没有重载的下标运算符,所以你不能做 set[0] 的例子。

我找到了办法:

auto myClass = *std::next(set.begin(), index);

但是,我发现一遍又一遍地复制该代码很单调。所以我决定只扩展std::set (class sset) 并重载其中的下标运算符会更方便。

template <class T>

class sset: public std::set<T>

public:
    T operator[](const uint32_t i) const
    
        if(i <= (this->size()-1))
            return *std::next(this->begin(), i);
        else
            throw std::out_of_range("Index is out of range");
    
;

int main()

    auto myClass = set[0]; //works and no exception thrown

    return 0;

我实现了预期的行为,但我突然想到标准没有包含下标运算符一定是有原因的。当然不只是懒惰。

这样做是否有任何预先知道的缺点或未来可能出现的问题?

【问题讨论】:

下标重载对于 std::mapstd::unordered_map 这样的关联类型有意义,因为它模仿数组的行为,只是使用任意键类型而不是整数索引。但是集合不保存键/值对,它们只保存键。 someset["a"] 应该如何工作?您无法分配给它,因为没有与该键关联的值。它应该返回什么?再次,没有价值......这没有意义。因此,没有下标重载。 这是set,对吧?这意味着容器中的项目没有 fixed 索引。也就是说,set[2] 可能只是通过将一个元素插入到集合中而成为不同的对象。那么为什么你需要像这样通过数字索引来索引一个集合呢? 请注意,C++ 中的容器类并非旨在公开继承。如果您真的想使用这样一个特殊的集合容器(一开始是个坏主意,请参阅所有答案),那么您应该从std::set 私下继承或使用您委托所有必需操作的数据成员。 @ChristianHackl: std::stackstd::queue,带有受保护的成员,显然是为了继承而设计的。我知道当没有虚拟析构函数时,公共继承是禁忌的,这是一种误导的概念。也许这就是你所指的? @Cheersandhth.-Alf:是的,我指的是“错误的”概念,即没有虚函数的类通常不应该被公开继承。 【参考方案1】:

索引不应超过对数时间,这是预期的。该索引是(至少)线性时间。这是非常低效的。如果您使用该索引遍历一组中的所有项目,您将获得二次总时间。这是一个很好的理由不这样做。


对于显示的代码,请注意

if(i <= (this->size()-1)

不能很好地处理大小 0。在这种情况下,您将获得无符号环绕,因此条件为true。取消引用结束迭代器就是未定义的行为。

【讨论】:

"索引不应超过对数时间,这是预期的。" 实际上,预期的是恒定时间。 Range TS 甚至通过声明 Random Access Ranges 提供 operator[] 支持来对此进行编码,该函数为 O(1)。 @NicolBolas:这些期望并非不相容。在某些情况下,使用对数时间索引运算符可能是合理的。最常见的例子是std::map 的索引运算符。【参考方案2】:

std::set 通常没有有效的方法来访问第 n 个元素。您使用的方法将从集合的开头开始,一次推进一个元素,直到它到达第 n 个。对于大型集合,这将非常慢。

如果您需要它,那么一定要这样做,但要注意效率低下。

【讨论】:

【参考方案3】:

除了已经提到的效率问题之外,std::set(与大多数其他标准容器一样)并非旨在继承自——尤其是,它不提供虚拟析构函数,因此以下必然会失败:

std::set<MyType>* s = new sset<MyType>();
delete s;

当然,以这种方式创建集合应该没有什么理由,但问题仍然存在......

如果你真的,真的需要nth 元素并且不想一直重写你的示例代码,我宁愿有一个单独的函数而不是(至少)有问题的继承:

template <typename T>
T& at(std::set<T>& s, size_t index)

    if(i < s.size())
        return *std::next(s.begin(), index);
    throw std::out_of_range("index is out of range");

template <typename T>
T const& at(std::set<T> const& s, size_t index)

    if(i < s.size())
        return *std::next(s.begin(), index);
    throw std::out_of_range("index is out of range");

永远不要在从 0 迭代到 size 的 for 循环中使用它,而是使用迭代器,或者最好使用基于范围的循环(它实际上映射到使用迭代器的循环)。

【讨论】:

以上是关于是否有任何理由不扩展 std::set 以添加下标运算符?的主要内容,如果未能解决你的问题,请参考以下文章

是否有任何理由对同一资源使用“Vary: *”和“Vary: Foo”响应?

是否有任何理由使用 C 而不是 C++ 进行嵌入式开发? [关闭]

是否有任何理由更喜欢数据挖掘项目的函数式编程? [关闭]

是否有理由不在 Core Data 实体中使用相同的属性名称?

有没有理由不使用OrderedDict?

是否有任何陷阱或充分的理由不使用 autosproc 进行存储过程调用?