आदिम static_vector कार्यान्वयन में संभावित अपरिभाषित व्यवहार


12

tl; dr: मुझे लगता है कि मेरे static_vector के पास अपरिभाषित व्यवहार है, लेकिन मैं इसे नहीं ढूँढ सकता।

यह समस्या Microsoft Visual C ++ 17 पर है। मेरे पास यह सरल और अधूरा static_vector कार्यान्वयन है, अर्थात एक निश्चित क्षमता वाला वेक्टर जो स्टैक आवंटित किया जा सकता है। यह एक C ++ 17 प्रोग्राम है, जो std :: align_storage और std :: launder का उपयोग करता है। मैंने इसे उन हिस्सों से नीचे उबालने की कोशिश की है जो मुझे लगता है कि इस मुद्दे के लिए प्रासंगिक हैं:

template <typename T, size_t NCapacity>
class static_vector
{
public:
    typedef typename std::remove_cv<T>::type value_type;
    typedef size_t size_type;
    typedef T* pointer;
    typedef const T* const_pointer;
    typedef T& reference;
    typedef const T& const_reference;

    static_vector() noexcept
        : count()
    {
    }

    ~static_vector()
    {
        clear();
    }

    template <typename TIterator, typename = std::enable_if_t<
        is_iterator<TIterator>::value
    >>
    static_vector(TIterator in_begin, const TIterator in_end)
        : count()
    {
        for (; in_begin != in_end; ++in_begin)
        {
            push_back(*in_begin);
        }
    }

    static_vector(const static_vector& in_copy)
        : count(in_copy.count)
    {
        for (size_type i = 0; i < count; ++i)
        {
            new(std::addressof(storage[i])) value_type(in_copy[i]);
        }
    }

    static_vector& operator=(const static_vector& in_copy)
    {
        // destruct existing contents
        clear();

        count = in_copy.count;
        for (size_type i = 0; i < count; ++i)
        {
            new(std::addressof(storage[i])) value_type(in_copy[i]);
        }

        return *this;
    }

    static_vector(static_vector&& in_move)
        : count(in_move.count)
    {
        for (size_type i = 0; i < count; ++i)
        {
            new(std::addressof(storage[i])) value_type(move(in_move[i]));
        }
        in_move.clear();
    }

    static_vector& operator=(static_vector&& in_move)
    {
        // destruct existing contents
        clear();

        count = in_move.count;
        for (size_type i = 0; i < count; ++i)
        {
            new(std::addressof(storage[i])) value_type(move(in_move[i]));
        }

        in_move.clear();

        return *this;
    }

    constexpr pointer data() noexcept { return std::launder(reinterpret_cast<T*>(std::addressof(storage[0]))); }
    constexpr const_pointer data() const noexcept { return std::launder(reinterpret_cast<const T*>(std::addressof(storage[0]))); }
    constexpr size_type size() const noexcept { return count; }
    static constexpr size_type capacity() { return NCapacity; }
    constexpr bool empty() const noexcept { return count == 0; }

    constexpr reference operator[](size_type n) { return *std::launder(reinterpret_cast<T*>(std::addressof(storage[n]))); }
    constexpr const_reference operator[](size_type n) const { return *std::launder(reinterpret_cast<const T*>(std::addressof(storage[n]))); }

    void push_back(const value_type& in_value)
    {
        if (count >= capacity()) throw std::out_of_range("exceeded capacity of static_vector");
        new(std::addressof(storage[count])) value_type(in_value);
        count++;
    }

    void push_back(value_type&& in_moveValue)
    {
        if (count >= capacity()) throw std::out_of_range("exceeded capacity of static_vector");
        new(std::addressof(storage[count])) value_type(move(in_moveValue));
        count++;
    }

    template <typename... Arg>
    void emplace_back(Arg&&... in_args)
    {
        if (count >= capacity()) throw std::out_of_range("exceeded capacity of static_vector");
        new(std::addressof(storage[count])) value_type(forward<Arg>(in_args)...);
        count++;
    }

    void pop_back()
    {
        if (count == 0) throw std::out_of_range("popped empty static_vector");
        std::destroy_at(std::addressof((*this)[count - 1]));
        count--;
    }

    void resize(size_type in_newSize)
    {
        if (in_newSize > capacity()) throw std::out_of_range("exceeded capacity of static_vector");

        if (in_newSize < count)
        {
            for (size_type i = in_newSize; i < count; ++i)
            {
                std::destroy_at(std::addressof((*this)[i]));
            }
            count = in_newSize;
        }
        else if (in_newSize > count)
        {
            for (size_type i = count; i < in_newSize; ++i)
            {
                new(std::addressof(storage[i])) value_type();
            }
            count = in_newSize;
        }
    }

    void clear()
    {
        resize(0);
    }

private:
    typename std::aligned_storage<sizeof(T), alignof(T)>::type storage[NCapacity];
    size_type count;
};

यह कुछ समय के लिए ठीक काम करता दिखाई दिया। फिर, एक बिंदु पर, मैं इसके साथ बहुत कुछ कर रहा था - वास्तविक कोड लंबा है, लेकिन यह इसके बारे में बताता है:

struct Foobar
{
    uint32_t Member1;
    uint16_t Member2;
    uint8_t Member3;
    uint8_t Member4;
}

void Bazbar(const std::vector<Foobar>& in_source)
{
    static_vector<Foobar, 8> valuesOnTheStack { in_source.begin(), in_source.end() };

    auto x = std::pair<static_vector<Foobar, 8>, uint64_t> { valuesOnTheStack, 0 };
}

दूसरे शब्दों में, हम पहले स्टैक पर एक static_vector में 8-बाइट फ़ॉबर ट्रैक्स की नकल करते हैं, फिर हम एक स्टैड बनाते हैं: 8-बाइट स्ट्रक्चर्स के स्टैटिक_वेक्टर की जोड़ी पहले सदस्य के रूप में, और दूसरे के रूप में एक uinto_t। मैं यह सत्यापित कर सकता हूं कि मानों का निर्माण किया गया है। स्टैक में जोड़ी बनाने से ठीक पहले सही मान शामिल हैं। और ... इस segfaults को static_vector की कॉपी कंस्ट्रक्टर (जिसे कॉलिंग फ़ंक्शन में इनबिल्ट किया गया है) के साथ जोड़ी बनाते समय सक्षम किया गया है।

लंबी कहानी छोटी है, मैंने डिसाइड्रेशन का निरीक्षण किया। यह जहां चीजें थोड़ी अजीब होती हैं; इनबिल्ड कॉपी कंस्ट्रक्टर के चारों ओर उत्पन्न asm नीचे दिखाया गया है - ध्यान दें कि यह वास्तविक कोड से है, ऊपर का नमूना नहीं है, जो बहुत करीब है, लेकिन जोड़ी निर्माण के ऊपर कुछ और सामान है:

00621E45  mov         eax,dword ptr [ebp-20h]  
00621E48  xor         edx,edx  
00621E4A  mov         dword ptr [ebp-70h],eax  
00621E4D  test        eax,eax  
00621E4F  je          <this function>+29Ah (0621E6Ah)  
00621E51  mov         eax,dword ptr [ecx]  
00621E53  mov         dword ptr [ebp+edx*8-0B0h],eax  
00621E5A  mov         eax,dword ptr [ecx+4]  
00621E5D  mov         dword ptr [ebp+edx*8-0ACh],eax  
00621E64  inc         edx  
00621E65  cmp         edx,dword ptr [ebp-70h]  
00621E68  jb          <this function>+281h (0621E51h)  

ठीक है, इसलिए पहले हमारे पास स्रोत से गंतव्य तक गिनती सदस्य की नकल करने के दो दो निर्देश हैं; अब तक तो सब ठीक है। edx शून्य है क्योंकि यह लूप वेरिएबल है। अगर गिनती शून्य है तो हमारे पास एक त्वरित जांच है; यह शून्य नहीं है, इसलिए हम लूप के लिए आगे बढ़ते हैं जहां हम 8-बाइट संरचना को कॉपी करते हैं, जिसमें दो 32-बिट मूवमेंट का उपयोग मेमोरी से रजिस्टर करने के लिए पहले किया जाता है, फिर रजिस्टर से मेमोरी में। लेकिन कुछ गड़बड़ है - जहाँ हम स्रोत वस्तु से पढ़ने के लिए [ebp + edx * 8 +] जैसी किसी चीज़ की अपेक्षा करेंगे, वहाँ इसके बजाय बस ... [ecx] है। यह सही नहीं लगता। Ecx का मूल्य क्या है?

पता चला है, एक्जॉक्स में सिर्फ एक कचरा पता होता है, वही जिसे हम सेगफॉल्ट कर रहे हैं। इसे यह मूल्य कहां से मिला? यहाँ तुरंत ऊपर है:

00621E1C  mov         eax,dword ptr [this]  
00621E22  push        ecx  
00621E23  push        0  
00621E25  lea         ecx,[<unrelated local variable on the stack, not the static_vector>]  
00621E2B  mov         eax,dword ptr [eax]  
00621E2D  push        ecx  
00621E2E  push        dword ptr [eax+4]  
00621E31  call        dword ptr [<external function>@16 (06AD6A0h)]  

यह एक नियमित पुराने सीडीसीएल फ़ंक्शन कॉल की तरह दिखता है। दरअसल, फंक्शन में ऊपर की तरफ बाहरी C फंक्शन का कॉल होता है। लेकिन ध्यान दें कि क्या हो रहा है: स्टैक पर तर्कों को आगे बढ़ाने के लिए एक अस्थायी रजिस्टर के रूप में ecx का उपयोग किया जा रहा है, फ़ंक्शन को आमंत्रित किया गया है, और ... तब तक ecx को फिर से कभी नहीं छुआ जाता है जब तक कि स्रोत static_vector से पढ़ने के लिए नीचे गलत तरीके से उपयोग नहीं किया जाता है।

व्यवहार में, एक्क्स की सामग्री को यहां बुलाए गए फ़ंक्शन द्वारा अधिलेखित कर दिया जाता है, जिसे यह निश्चित रूप से करने की अनुमति है। लेकिन यहां तक ​​कि अगर यह नहीं था, तो कोई रास्ता नहीं है, यहां तक ​​कि एक्स्टेक्स कभी भी यहां सही चीज़ के लिए एक पता रखने वाला है - सबसे अच्छा, यह एक स्थानीय स्टैक सदस्य को इंगित करेगा जो static_vector नहीं है। ऐसा लगता है जैसे संकलक ने कुछ फर्जी असेंबली का उत्सर्जन किया है। यह फ़ंक्शन कभी भी सही आउटपुट नहीं दे सकता है ।

इसलिए मैं अब वहीं हूं। अजीब असेंबली जब एसटीडी में आस-पास खेलते समय अनुकूलन सक्षम होते हैं :: लॉन्ड्रर भूमि मुझे अपरिभाषित व्यवहार की तरह बदबू आती है। लेकिन मैं यह नहीं देख सकता कि यह कहाँ से आ रहा है। पूरक लेकिन मामूली रूप से उपयोगी जानकारी के रूप में, सही झंडे के साथ क्लैंग इस के समान विधानसभा का उत्पादन करता है, सिवाय इसके कि मूल्यों को पढ़ने के लिए ईएक्सएक्स के बजाय ईबीपी + एडक्स का सही उपयोग करता है।


केवल एक सरसरी नज़र है लेकिन आप clear()उन संसाधनों पर क्यों कॉल कर रहे हैं जिन पर आपने कॉल किया है std::move?
बाथशीबा

मैं नहीं देखता कि यह कैसे प्रासंगिक है। निश्चित रूप से, यह static_vector को उसी आकार के साथ छोड़ने के लिए कानूनी होगा, लेकिन स्थानांतरित की गई वस्तुओं का एक गुच्छा। जब भी static_vector विध्वंसक चलता है तब सामग्री को नष्ट कर दिया जाएगा। लेकिन मैं स्थानांतरित आकार के वेक्टर को शून्य के आकार के साथ छोड़ना पसंद करता हूं।
पोहशानसन

हम। मेरे पे-ग्रेड से परे हैं। एक उत्थान है क्योंकि यह अच्छी तरह से पूछा जाता है, और ध्यान आकर्षित कर सकता है।
बाथशीबा

अपने कोड के साथ किसी भी दुर्घटना को पुन: उत्पन्न नहीं कर सकते (इसकी कमी के कारण इसका अनुपालन नहीं करने में मदद is_iterator) कृपया एक न्यूनतम प्रतिलिपि प्रस्तुत करने योग्य उदाहरण
एलन बर्टल्स

1
Btw, मुझे लगता है कि बहुत सारे कोड यहाँ अप्रासंगिक हैं। मेरा मतलब है, आप असाइनमेंट ऑपरेटर को यहां कहीं भी नहीं बुलाते हैं, इसलिए इसे उदाहरण से हटाया जा सकता है
बार

जवाबों:


6

मुझे लगता है कि आपके पास एक कंपाइलर बग है। दुर्घटना को ठीक करने के __declspec( noinline )लिए जोड़ना operator[]:

__declspec( noinline ) constexpr const_reference operator[]( size_type n ) const { return *std::launder( reinterpret_cast<const T*>( std::addressof( storage[ n ] ) ) ); }

आप Microsoft को बग की रिपोर्ट करने का प्रयास कर सकते हैं, लेकिन लगता है कि बग दृश्य स्टूडियो 2019 में पहले से ही तय हो गया है।

हटाने std::launderसे दुर्घटना को ठीक करना भी प्रतीत होता है:

constexpr const_reference operator[]( size_type n ) const { return *reinterpret_cast<const T*>( std::addressof( storage[ n ] ) ); }

मैं अन्य स्पष्टीकरणों पर भी कम चल रहा हूं। जितना उस चूसना ने हमारी वर्तमान स्थिति को देखते हुए, यह प्रशंसनीय लगता है कि यह वही है जो चल रहा है, इसलिए मैं इसे स्वीकृत उत्तर के रूप में चिह्नित करूंगा।
पोझानसन

लॉंडर को हटाने से यह ठीक हो जाता है? स्पष्ट रूप से अनुचित व्यवहार को हटाकर लॉन्ड्रिंग को हटाया जाएगा! अजीब।
पोझानसन

@pjohansson std::launderको कुछ कार्यान्वयन द्वारा गलत तरीके से लागू किया गया था। हो सकता है कि आपका MSVS का संस्करण उस गलत कार्यान्वयन पर आधारित हो। मेरे पास स्रोत नहीं हैं, दुर्भाग्य से।
मुरैना
हमारी साइट का प्रयोग करके, आप स्वीकार करते हैं कि आपने हमारी Cookie Policy और निजता नीति को पढ़ और समझा लिया है।
Licensed under cc by-sa 3.0 with attribution required.