在std :: vector上扩展成员选择运算符` - >`的实现是否正确?

时间:2015-12-16 08:29:07

标签: c++ pointers iterator operators stdvector

问题

如果value_type是指针类型,则迭代器的成员选择运算符->是无用的,因为访问成员需要间接:(*_It)->member。如果有代码执行_It->method()之类的操作,则将包含项的类型更改为指针类型会变得很麻烦。

这是因为成员选择运算符->的原始实现是:

// return pointer to class object
pointer operator->() const { return (&**this); }

提议的解决方案

如果value_type是指针类型,则成员选择运算符应返回reference而不是pointer

阐述实施:
我刚刚将跟随扩展插入<utility>

template<typename val, typename ref, typename ptr>
struct ref_or_ptr {
    // To know if value_type is a pointer
    enum {RES=false};
    // By default, operator -> should return a pointer
    typedef ptr res; // return pointer
    // The translation function sould be used to return proper type
    // in both the cases, otherwise the complier results in the conversion error.
    static res get(ref v) { return &v; }
};

// Specialization for the case when value_type is a pointer
template<typename val, typename ref, typename ptr>
struct ref_or_ptr<val *, ref, ptr> {
    enum {RES=true}; // value_type is a pointer type
    typedef ref res; // return reference
    // In this case, the reference is returned instead of the pointer.
    // But here value_type is a pointer type, and the address-of operator is not needed:
    static res get(ref v) { return v; }
};

我已通过我的扩展实现替换了-><vector> _Vector_const_iterator中成员选择运算符_Vector_iterator的原始实现:

typename ref_or_ptr<value_type, reference, pointer>::res
operator->() const {
    return (ref_or_ptr<value_type, reference, pointer>::get(**this));
}

现在,以下内容成为可能,保留了成员选择运算符的原始行为:

#include <stdio.h>
#include <tchar.h>
#include <iostream>
#include <vector>

typedef struct udf {
    static int auto_m; int _m;
    udf(): _m(++auto_m) {}
} * udf_ptr;
int udf::auto_m = 0;

int _tmain(int argc, _TCHAR* argv[]) {
    using namespace std;
    typedef vector<udf_ptr> udf_p_v;
    typedef vector<udf> udf_v;
    udf_v s(1); udf_p_v p(1);
    p[0] = &s[0];
    udf_v::iterator s_i(s.begin()); udf_p_v::iterator p_i(p.begin());

    // Now, the indirection on p_i is not needed
    // in order to access the member data _m:
    cout
        << "s_i: " << s_i->_m << endl
        << "p_i: " << p_i->_m << endl;

    return 0;
}

输出:

s_i: 1
p_i: 1

两个问题

是的,如果代码应由其他人编译,则替换STL模板的原始实现是一件坏事。如果版权所有者或许可允许,可以通过提供<utility><vector>的扩展版本来解决此问题。否则将无法分发此类软件。

  1. 那么,以所描述的方式在指针上扩展成员选择运算符的原始实现是否正确?对于内部软件,它可能是正确的。如果源代码应该是开放的,或者是那些无法获得这些扩展版本的人使用的话,那就不是了。这种情况是什么?还有其他案例吗?

  2. 是否存在建议的扩展实施不起作用的情况?

5 个答案:

答案 0 :(得分:3)

你不需要为此编写任何代码 - Boost已经拥有它!见这里:http://www.boost.org/doc/libs/1_59_0/libs/ptr_container/doc/tutorial.html

他们的例子:

typedef boost::ptr_vector<animal>  ptr_vec;
ptr_vec vec;
ptr_vec::iterator i = vec.begin();
i->eat(); // no indirection needed

答案 1 :(得分:2)

这里有一些很好的答案,但我觉得你的问题和建议的解决方案没有得到解决。

  

请引用标准

当然,无论如何(强调我的)

  
    

17.6.4.2.1命名空间std [namespace.std]

         

如果C ++程序在命名空间std 中添加声明或定义或在命名空间std中添加名称空间,则未定义,除非另有说明。只有当声明取决于用户定义的类型且专业化符合原始模板的标准库要求且未明确禁止时,程序才可以将任何标准库模板的模板特化添加到命名空间std。

         

2如果C ++程序声明

,则其行为是未定义的          

(2.1) - 标准库类模板的任何成员函数的显式特化,或

         

(2.2) - 标准库类或类模板的任何成员函数模板的显式特化,     或

         

(2.3) - 标准库类或类模板的任何成员类模板的显式或部分特化。

  

还有更多,链接是here(第459页)

我认为条款2.2和2.3很好地涵盖了这一点。

这些都不是个人的 - 它的意思是以幽默的方式阅读。但是,这个消息非常严重。

如果任何年轻的开发者都有同样的想法......

  

我刚将以下扩展名插入<utility>

恭喜,你刚刚破解c ++ 发明了一种新语言,它看起来很像c ++,但有一些微妙的,无证的差异,并没有国际标准的支持。我们称之为Aleksey ++: - )

在C ++中,您在std::命名空间中唯一可以做的就是为您自己的用户定义类型提供专门化模板。您不得添加,删除或修改其中的任何概念。您可能不会通过修改标准库的文件来做到这一点。它们应该是只读的!

  

我已通过我的扩展实施替换了operator -><vector> _Vector_const_iterator中成员选择_Vector_iterator的原始实现

你所做的是破坏了你的c ++安装(见上文)。你是一个非常顽皮的男孩。

  

是的,如果代码应由其他人编译,则替换STL模板的原始实现是一件坏事。

这是你做过的一件非常邪恶的事。

  

如果版权所有者或许可允许,可以通过提供<utility><vector>的扩展版本来解决。

c ++标准本身不允许这样做。

  

否则将无法分发此类软件。

至少以源代码形式分发此类软件始终是不可能的。

  

那么,以所描述的方式在指针上扩展成员选择运算符的原始实现是否正确?

不,不,三次没有!

  

对于内部软件,它可能是正确的。

不,这绝对不正确。如果内部其他人想要编写另一个符合标准的程序,你知道,就像其他人总是写的一样,该怎么办?你已经破坏了他们的编译器!

  

如果源代码应该是打开的,或者是那些无法获得这些扩展版本的人使用的话,那就不是了。这种情况是什么?还有其他案例吗?

只需根据标准编写软件,其他人就不必破坏其编译器安装。如果您的代码依赖于非标准扩展程序&#39;在标准库中,任何人都不会用驳船接触它。

  

是否存在建议的扩展实施不起作用的情况?

每次升级编译器套件时,请转到另一台计算机,获取新版本的标准库,共享您的软件。 5年后,当你离开公司并且一些可怜的家伙试图维护你的软件时,他会想到你的非常糟糕的想法。

  你是什​​么意思?所有内部项目的所有代码都已成功编译

他们目前可能在内部编译,但他们并没有使用符合标准的c ++进行编译。这使得它们不易携带且易碎。例如,如果您的某位同事将其编译器升级到更高版本,则代码将不再编译。

如果他随后来到这里并询问&#34;为什么这段代码没有编译?&#34;我们都将他的代码插入到我们的编译器中并得出结论错误地编写了代码。然后他会抗议,说它以前总是奏效 - 也许他的编译器坏了。我们都会神秘化。将提交错误报告。编译器供应商将(最终)回复编译器没问题,代码错误。

此时你的同事会意识到这根本不是他的工具集。事实上,代码是用Aleksey ++而不是C ++编写的。

他不会对你感到满意......

  

在此插入其他抗议

标准库的重点在于它是......标准的。如果你想创建一个看起来很像标准库但是部分不同的库,你应该将它放在一个新的命名空间中,比如namespace aleksey { }

std命名空间绝对是为标准库保留的,它应该完全符合标准(在其他地方here发布)。

扩展是按定义的,而不是标准的,因此不应该在std命名空间中。

此命名空间与编译器有非常密切的关系。您可以使用typeid来证明这一点,而不包括标准标题type_info。在clang上你会收到警告。这是因为标准说为了使用typeid ,您必须首先包含<type_info>。不这样做会使程序生成错误,此后它的行为未定义

这些术语用斜体字表示,因为它们指的是标准中的特定含义。

  

好的,我们如何前进?

在新命名空间中定义所需的功能。谁知道,它可能是如此可怕,以至于我们都想一直使用它。众所周知的图书馆boost是这样的。 boost中的许多功能都包含在标准中,因为数百万开发人员在数以百万计的项目中使用后,它们被证明是非常棒的。

其中一些功能是:(非详尽列表)

boost::shared_ptr<> -> adopted as std::shared_ptr
boost::thread -> adopted as std::thread
boost::hash -> adopted (in part) as std::hash
boost::unordered_map -> std::...
boost::unordered_set -> std::...
boost::lambda -> incorportated directly into the language
boost::move -> incorportated directly into the language

列表很长并且还在继续增长。

答案 2 :(得分:1)

  1. 我不认为这是个好主意。标准库是标准的原因,人们希望它以标准方式运行。打破这种期望可能是危险的。我想说制作你自己的迭代器或容器类,或者使用其他库提供的那些是一个非常优秀的解决方案。

  2. 我想这样做可能会打破一些标准算法。

  3. 此问题的另一个Boost提供的解决方案是boost::indirect_iterator。以下代码John Zwinck发布的内容如下:

    std::vector<animal*> vec;
    vec.push_back(new animal{});
    auto i = boost::make_indirect_iterator(std::begin(vec));
    i->eat(); // no indirection needed
    

    这甚至适用于std::vector<std::unique_ptr<animal>>,如果你想迭代一个成员容器并隐藏你正在存储std::unique_ptr的事实,这可能非常方便。

答案 3 :(得分:1)

C ++标准定义了std::vector<T>及其迭代器的语义。它指定了std::vector<T>迭代器中涉及的各种类型的类型。更改实施将违反标准合规性。

无论如何还有另一种扭曲:虽然T*是明显的指针类型,但std::vector<T>实际上包含一系列智能指针并不常见,例如,std::unique_ptr<T> 。类似,适用于std::vector<T>的内容也适用于std::deque<T>std::list<T>等。

虽然逻辑可以折叠到各种容器的实现中,但它实际上更简单,并且通常更灵活(因为可能需要其他定制),而是使用合适的迭代器适配器和合适的函数来获取这些。

指向通用性的第一步是检测智能指针。用一个简单的特性很容易做到:

template <typename T>
struct is_pointer_like_test {
    template <typename S, typename = decltype(std::declval<S>().operator->())>
    static std::true_type  test(S*);
    template <typename S>
    static std::true_type  test(S**);
    static std::false_type test(void*);
};

template <typename T>
struct is_pointer_like
    : decltype(is_pointer_like_test<T>::test(static_cast<T*>(0))) {
};

这个特性认为原始指针和任何重载operator->()都是指针类型,其他一切都是非指针类型。同一个主题可能有一些变化更加聪明。

由于指针的类型和访问权限与值不同,但两个变体都需要在迭代器适配器中编译(除非迭代器适配器是专用的),因此抽象出差异很方便:

template <typename T, bool = is_pointer_like<T>::value>
struct pointer_helper {
    using type = T;
    template <typename S>
    static S value(S&& arg) { return std::forward<S>(arg); }
};
template <typename T>
struct pointer_helper<T, true> {
    using type = typename std::decay<decltype(*std::declval<T>())>::type;
    template <typename S>
    static auto value(S&& arg) -> decltype(*arg) { return *arg; }
};

我的想法是获取指针的指针类型,但保留类型本身它不是指针。同样,指针被取消引用以访问值,而非指针则不会。这是相对较快的,我想它可以改进。

下一步是将这些构建块放在一起以创建指针适配器和一些工厂函数:

template <typename Iterator>
class ptr_iterator {
    Iterator it;
    using original_value = typename std::iterator_traits<Iterator>::value_type;

public:
    using iterator_category = typename std::iterator_traits<Iterator>::iterator_category;
    using difference_type = typename std::iterator_traits<Iterator>::difference_type;
    using value_type = typename pointer_helper<original_value>::type;
    using reference  = typename std::add_lvalue_reference<value_type>::type;
    using pointer    = typename std::add_pointer<value_type>::type;

    explicit ptr_iterator(Iterator it): it(it) {}
    bool operator==(ptr_iterator const& other) const { return this->it == other.it; }
    bool operator!=(ptr_iterator const& other) const { return !(*this == other); }
    ptr_iterator& operator++() { ++this->it; return *this; }
    ptr_iterator  operator++(int) { ptr_iterator rc(*this); ++this->it; return rc; }

    reference operator*() { return pointer_helper<original_value>::value(*it); }
    pointer   operator->() { return &**this; }
    // other iterator operations
};

template <typename Cont>
auto ptr_begin(Cont&& cont) -> ptr_iterator<decltype(cont.begin())> {
    return ptr_iterator<decltype(cont.begin())>(cont.begin());
}

template <typename Cont>
auto ptr_end(Cont&& cont) -> ptr_iterator<decltype(cont.end())> {
    return ptr_iterator<decltype(cont.begin())>(cont.end());
}

我认为上面应该是一个可行的前向迭代器,但它显然缺少将其转换为双向或随机访问迭代器的各种操作。与pointer_helper一样,此适配器用于大致演示它的外观,而不是一个可靠的,经过测试的解决方案。

有了这些(并假设一组合适的标准头包括),应该可以处理[智能]指针的容器和值相同,例如:

struct foo {
    int value;
    foo(int value): value(value) {}
    void bar() { std::cout << "foo(" << value << ")\n"; }
};

template <typename Cont>
void print(Cont&& cont) {
    for (auto it = ptr_begin(cont), end = ptr_end(cont); it != end; ++it) {
        it->bar();
    }
}

int main()
{
    std::vector<foo> v;
    v.push_back(1);
    v.push_back(2);
    v.push_back(3);
    print(v);

    std::vector<foo*> p;
    p.push_back(&v[2]);
    p.push_back(&v[1]);
    p.push_back(&v[0]);
    print(p);

    std::vector<std::unique_ptr<foo> > s;
    s.push_back(std::unique_ptr<foo>(new foo(4)));
    s.push_back(std::unique_ptr<foo>(new foo(5)));
    s.push_back(std::unique_ptr<foo>(new foo(6)));
    print(s);
}

除了让用例2工作之外,代码还没有经过特别好的测试。

答案 4 :(得分:0)

我同意其他的回答者认为修改标准库是一个非常糟糕的主意。

问题是如何在将集合成员类型从value-type更改为pointer-type时最小化对现有代码的更改。我可以想到以下解决方案:

  1. 将容器类型使用的迭代器类型更改为某种形式 间接迭代器,它自动解引用它的指针 指着。这基本上是你对std :: vector的修改 等于。更好的方法是从中派生出一个类 向量并更改begin()返回的迭代器类型, end()等等甚至更好(如果对现有代码进行一些修改) 允许),添加函数ibegin()iend(),返回 必要的间接迭代器。如果允许更多修改, 那么你应该考虑使用像间接迭代器这样的东西 根据TartanLlama的回答,由Boost定义的变换器。从长远来看,这可能就是你想成为的地方。
  2. 更改您计划替换的指针类型 容器中的value-type。这是一个更丑陋的解决方案 需要一定数量的代码重复,但可以使用 没有修改现有代码。请参阅下面的代码以制作您的代码 给出示例工作除了删除使用之外没有任何更改 _TCHAR。
  3. 见下文:

    #include <iostream>
    #include <vector>
    
    using namespace std;
    
    template<class T>
    class pass_thru_ptr{
      T* ptr;
    public:
      T& operator*() { return *ptr; }
      pass_thru_ptr& operator=(T* p) { ptr = p; return *this; }
      //  define other operators, such as:
      //  pass_thru_ptr& operator=(T p) { *ptr = p; return *this; }
    };
    
    template<class T>
    ostream& operator<<(ostream& out, pass_thru_ptr<T> p){
      return out << (*p);
    }
    
    typedef struct udf {
      static int auto_m; int _m;
      udf(): _m(++auto_m) {}
    } * udf_ptr;
    int udf::auto_m = 0;
    
    // or template specialization?
    struct pass_thru_udf_ptr : public pass_thru_ptr<udf> {
      pass_thru_ptr<int> _m;
      pass_thru_udf_ptr& operator=(udf* p) { pass_thru_ptr<udf>::operator=(p); _m = &(p->_m); return *this; }
    };
    
    int main(int argc, char* argv[]) {
      using namespace std;
      typedef vector<pass_thru_udf_ptr> udf_p_v;
      typedef vector<udf> udf_v;
      udf_v s(1); udf_p_v p(1);
      p[0] = &s[0];
      udf_v::iterator s_i(s.begin()); udf_p_v::iterator p_i(p.begin());
    
      // Now, the indirection on p_i is not needed
      // in order to access the member data _m:
          cout
            << "s_i: " << s_i->_m << endl
            << "p_i: " << p_i->_m << endl;
    
          return 0;
    }