重载模板化类的[]运算符的常量版本



关于在C++中重载[]运算符的另一个问题,特别是它的const版本。

根据关于运算符重载的cppreference页面,当重载数组下标运算符时

struct T
{
value_t& operator[](std::size_t idx)       { return mVector[idx]; }
const value_t& operator[](std::size_t idx) const { return mVector[idx]; }
};

如果已知值类型是内置类型,则const变量应按值返回。

因此,如果value_t恰好是内置类型,则const变体应该看起来是

const value_t operator[](std::size_t idx) const { return mVector[idx]; }

甚至可能是

value_t operator[](std::size_t idx) const { return mVector[idx]; }

因为CCD_ 5限定符对于这样的返回值不是很有用。


现在,我有一个模板化的class T(保持与引用相同的命名),它被和内置数据类型以及用户定义的数据类型使用,其中一些可能很重。

template<class VT>
struct T
{
VT& operator[](std::size_t idx)       { return mVector[idx]; }
const VT& operator[](std::size_t idx) const { return mVector[idx]; }
};

根据上面给出的建议,我应该使用enable_if和一些type_traits来区分具有内置/非内置类型的模板化类实例化。

我必须这么做吗?这项建议是否只是为了避免对内置类型潜在的不必要的取消引用,或者背后隐藏着其他应该注意的东西?


备注:

  • 这个类积极参与代码的热点部分,该部分用内置类型和自定义类型实例化
  • 代码跨平台使用,多个编译器具有不同程度的优化选项
  • 因此,我有兴趣使它既正确又正确;便携式,并避免任何潜在的性能损害
  • 我在C++标准中找不到任何额外的推理,但阅读standardeze并不是我最擅长的

StackOverflow上存在的问题:

关于运算符重载的基本规则和习惯用法的C++FAQ条目建议在没有明确理由的情况下使用"应该更好"构造返回副本。这个"应该更好"与cppreference页面中的"应该"略有不同,这让我感到困惑
  • 这个和这个从原理上讲const重载;然而,并不是关于我感兴趣的细节。此外,第一个使用了const value_t&,第二个使用const value_t
  • 我不同意上面的"建议"。考虑一下:

    T t = /*Initialize `t`*/;
    const T::value_t &vr = std::as_const(t)[0];
    const auto test = vr; //Copy the value
    t[0] = /*some value other than the original one.*/
    assert(test != vr);
    

    断言是否触发?它不应该触发,因为我们只是在引用容器中的值。基本上,std::as_const(t)[i]应该具有与std::as_const(t[i])相同的效果。但如果您的const版本返回一个值,则不会。因此,进行这样的更改从根本上改变了代码的语义。

    因此,即使您知道value_t是一个基本类型,您仍然应该返回一个const&

    注意,C++20范围正式识别不从其operator*或等效函数返回实际value_type&的范围。但即便如此,这些东西也是该范围本质的一个基本部分,而不是一个基于模板参数而变化的属性(请参阅vector<bool>,了解为什么这是一个坏主意)。

    您不需要以特殊的方式处理基本类型。对于非const变体,总是简单地返回value_t&,对于const变体,总是返回const value_t&

    重载通常很短,就像您的例子一样,所以它们无论如何都会在每个调用站点内联。在这种情况下,重载是通过值返回还是通过引用返回都无关紧要,在任何一种情况下,间接性都将被优化掉。任何设置为至少低优化级别的现代编译器都应该这样做。

    我想不出有任何其他理由以不同的方式处理基本类型。


    我还要提醒您,如果您像示例中那样实现容器类,则返回引用和返回值对用户来说将具有不同的语义。

    如果返回对元素的const引用,则用户可以保留该引用并观察容器元素中的更改(直到引用以某种指定方式失效为止)。如果返回一个值,则无法观察到变化。

    如果用户能够获得某些类型的引用并观察到未来的变化,但不能观察到其他类型的变化,那将是令人惊讶的。最糟糕的情况是,如果他们也使用类型泛型代码,他们还需要对所有模板进行条件设置。

    此外,即使对于基本类型按值返回,也只能通过对对象的const引用来调用按值返回的重载。在大多数情况下,用户可能有一个容器的非const实例,为了利用这种潜在的优化,他们需要在调用运算符重载之前先将对象引用显式转换为const

    因此,如果优化是一个问题,我宁愿添加一个额外的成员函数,总是按值返回容器元素的副本,如果潜在的取消引用被确定为性能问题,则用户可以使用该函数。调用这个成员函数不会比确保调用正确的运算符重载更麻烦。

    最新更新