从 member-class 调用封装的 std::begin 时程序崩溃

Program crashes, when calling capsulated std::begin from member-class

我的生产代码崩溃了,我找不到原因, 所以我寻找罪恶的根源,现在成功地构建了一个最小的例子,其中变体 B 重现了错误。

也许有人可以帮助我了解这里的问题是什么?

为什么变体 B 会崩溃而变体 A 不会?对我来说,这两个变体似乎应该具有相同的行为?

visual studio 2017 调试器的输出:

void _Adopt(const _Container_base12 *_Parent) _NOEXCEPT
        {   // adopt this iterator by parent
        if (_Parent == 0)
            {   // no future parent, just disown current parent
 #if _ITERATOR_DEBUG_LEVEL == 2
            _Lockit _Lock(_LOCK_DEBUG);
            _Orphan_me();
 #endif /* _ITERATOR_DEBUG_LEVEL == 2 */
            }
        else
            {   // have a parent, do adoption
            _Container_proxy *_Parent_proxy = _Parent->_Myproxy;

 #if _ITERATOR_DEBUG_LEVEL == 2
            if (_Myproxy != _Parent_proxy)
                {   // change parentage
                _Lockit _Lock(_LOCK_DEBUG);
                _Orphan_me();
                _Mynextiter = _Parent_proxy->_Myfirstiter;   <--- MARKED HERE
                _Parent_proxy->_Myfirstiter = this;
                _Myproxy = _Parent_proxy;
                }

 #else /* _ITERATOR_DEBUG_LEVEL == 2 */
            _Myproxy = _Parent_proxy;
 #endif /* _ITERATOR_DEBUG_LEVEL == 2 */
            }
        }

最小的可编译示例:

#include <vector>

using namespace std;

template <class T>
struct IteratorCapsule
{
    const T * m_ptrObj;
    IteratorCapsule(const T &refObj) : m_ptrObj(&refObj) {}
    typename T::const_iterator begin() const {  return std::begin(*m_ptrObj); }
    typename T::const_iterator end() const { return std::end(*m_ptrObj); }
};

struct Element
{
    vector<int> m_attr;
    IteratorCapsule<vector<int>> m_attr_iter_capsule;

    Element() : m_attr_iter_capsule(m_attr) {}

    const IteratorCapsule<vector<int>> &getAttributes() const
    {
        return m_attr_iter_capsule;
    }
};

struct Config
{
    vector<Element> m_element_pool;
    Config() { m_element_pool.push_back(Element()); }
};

int main()
{
    //variant A
    Config oConfigA;
    IteratorCapsule<vector<int>> oIterCapsule(oConfigA.m_element_pool[0].m_attr);
    auto iterBeginA = oIterCapsule.begin();

    //variant B -> crash 
    Config oConfigB;
    auto iterBeginB = oConfigB.m_element_pool[0].getAttributes().begin();

    return 0;
}

让我们尝试显式实现 Element(Element&& x) 看看会发生什么。

Element(Element&& x) : m_attr (std::move(x.m_attr)),
    m_attr_iter_capsule (std::move(x.m_attr_iter_capsule)) // WRONG
{}

当(但不仅限于)Element 从右值构造时调用,例如在本例中:

Element a {std::move(Element())};

在这种情况下 a.m_attr_iter_capsule 将包含指向内部 Element() 的指针(超出该行代码之后的范围)

(请注意,取消引用越界指针是未定义的行为。不保证它会发生段错误)

在 OP 的代码中,这一行

m_element_pool.push_back(Element());

调用 Element(Element&&)。问题可以是 "silenced" 通过使用 emplace_back() (但这不是这个答案的重点)。

要修复它,只需定义显式复制和移动构造函数,并适当地初始化 m_attr_iter_capsule;或使用 = delete.

删除那些构造函数

最后,这里是带有显式移动构造函数和删除的复制赋值运算符、移动赋值运算符、复制构造函数的工作解决方案。

#include <iostream>
#include <vector>

template <class T>
struct IteratorCapsule
{
    const T * m_ptrObj;
    IteratorCapsule(const T &refObj) : m_ptrObj(&refObj) {}
    typename T::const_iterator begin() const {  return std::begin(*m_ptrObj); }
    typename T::const_iterator end() const { return std::end(*m_ptrObj); }
};

struct Element
{
    std::vector<int> m_attr;
    IteratorCapsule<std::vector<int>> m_attr_iter_capsule;

    Element(const Element&) = delete;
    Element& operator=(const Element&) = delete;
    Element& operator=(Element&&) = delete;
    explicit Element() noexcept : m_attr_iter_capsule(m_attr) { m_attr.push_back(666); }
    Element(Element&& x) noexcept : m_attr(std::move(x.m_attr)), m_attr_iter_capsule(m_attr) {}
    ~Element() noexcept {};

    const IteratorCapsule<std::vector<int>> &getAttributes() const
    {
        return m_attr_iter_capsule;
    }
};

struct Config
{
    std::vector<Element> m_element_pool;
    Config() { m_element_pool.push_back(Element()); }
};

int main()
{
    //variant A
    Config oConfigA;
    IteratorCapsule<std::vector<int>> oIterCapsule(oConfigA.m_element_pool[0].m_attr);
    auto iterBeginA = oIterCapsule.begin();
    std::cout << "Variant A: " << *iterBeginA << std::endl;

    //variant B -> works now! 
    Config oConfigB;
    auto iterBeginB = oConfigB.m_element_pool[0].getAttributes().begin();
    std::cout << "Variant B: " << *iterBeginB << std::endl;

    return 0;
}