Beste Art ein Array zurückzuliefern


  • Mod

    ein auto_array ist ja ganz nett, aber hat eben leider den nachteil, dass die referenzsemantik überall sichtbar bleibt, ausserdem müssen wir uns noch irgendwie die grösse merken (wenn diese statisch wäre, arbeitet man gleich mit structs (siehe boost::array). also etwa so (entwurf)

    typedef unsigned long long u64; // zwecks alignment
    template<class T, class X>
    struct enable_if_same
    {
    };
    
    template<class X>
    struct enable_if_same<X, X>
    {
        typedef char type;
    };
    
    template<typename T>
    class Array
    {
    private:
    // wir nehmen union hier, um uns etwas schreibarbeit zu sparen
    // wenn T ein POD ist, können wir es nat. einfacher haben - da es dort keinen unterschied macht, ob wir zwischen copy-c'tor oder copy-assign verwenden
        union
        {
            u64* mem_;
            T* array_;
        };
        std::size_t size_;
        struct Ref
        {
            Array* ref_;
            Ref(Array* ref) : ref_( ref ) {}
        };
        operator Ref() { return Ref( this ); } // non-const
    public:
        Array() : array_(), size_() {}
        explicit Array(std::size_t size)
        : mem_( new u64[ size_ * sizeof T / sizeof u64 ] ), size_( size ) {}
    // Konstruktoren zum Kopieren
        // non-const lvalues - deep copy
        Array(Array& rhs)
        : mem_( new u64[ rhs.size_ * sizeof T / sizeof u64 ] ), size_( rhs.size_ )
        {
    // jetzt in abhängigkeit von T:
            for ( std::size_t i = 0; i < size_; ++i )
            {
                new( array_ + i ) T( rhs.array_[ i ] );
            }
    // oder per memcpy falls T ein POD ist oder memcopyable ist
    // evtl. noch Duff's device für loop-unrolling
        }
        // non-const rvalues (z.B. Funktionsergebnisse) - move
        Array(Ref ref) : mem_( ref.ref_->mem_ ), size_( ref.ref_->size_ )
        {
            ref.ref_->mem_ = 0;
        }
    // const lvalue - deep copy
        template <class T>
        Array(T& rhs, typename enable_if_same<Array const,T>::type = 0)
        : // ...
        {
             // siehe Array(Array& rhs) c'tor
        }
    
        ~Array()
        {
    // wenn T einen trivialen destruktor hat, können wir uns das hier sparen:
            for ( std::size_t i = 0; i < size_; ++i )
            {
                array_[ i ].~T();
            }
    
            delete [] mem_;
        }
    
        Array& operator=(Array rhs)
        {
            if ( mem_ != 0 ) delete [] mem_;
            mem_ = rhs.mem_;
            rhs.mem_ = 0;
        }
    
        T& operator[](std::size_t i) { return array_[ i ]; }
        const T& operator[](std::size_t i) const { return array_[ i ]; }
    
        std::size_t size() const { return size_; }
    };
    

    und schon haben wir ein array, dass sich normal kopieren lässt, aber das kopieren beim rückgeben per funktion vermeidet, so wie es auto_array täte.



  • @camper & @me: Wieso eigentlich nicht gleich einen std::vector zurückgeben?



  • camper schrieb:

    das kopieren beim rückgeben per funktion

    Soweit ich weiß können manche Compiler das wegoptimieren.



  • Hi,

    warum nicht einfach auf bestehendes wie boost::shared_array oder boost::scoped_array zurückgreifen?

    boost::shared_array<int> MyFuncShared()
    {
       boost::shared_array<int> arr( new int[10] );
       for( size_t i = 0; i < 10; ++i)
          arr[i] = i; 
       return arr;
    }
    boost::scoped_array<int> MyFuncScoped()
    {
       boost::scoped_array<int> arr( new int[10] );
       for( size_t i = 0; i < 10; ++i)
          arr[i] = i; 
       return arr;
    }
    


  • evilissimo schrieb:

    Hi,

    warum nicht einfach auf bestehendes wie boost::shared_array oder boost::scoped_array zurückgreifen?

    boost::shared_array<int> MyFuncShared()
    {
       boost::shared_array<int> arr( new int[10] );
       for( size_t i = 0; i < 10; ++i)
          arr[i] = i; 
       return arr;
    }
    boost::scoped_array<int> MyFuncScoped()
    {
       boost::scoped_array<int> arr( new int[10] );
       for( size_t i = 0; i < 10; ++i)
          arr[i] = i; 
       return arr;
    }
    

    Vorteil gegenüber std::vector?



  • Es werden weniger daten kopiert und bei einem zeiger müsste man auch auf die ownership achten.



  • Es werden weniger daten kopiert

    Der Compiler macht aus

    std::vector<int> MyFuncShared()

    sowieso

    void MyFuncShared(std::vector<int>& v)

    also vom kopieren ändert sich nichts.



  • Wer garantiert mir dass das alle Compiler tun?



  • niemand...aber so wichtig ist die garantie ja nicht wenn es mal auf nem uralt-compiler nicht so gemacht wird. 🤡


  • Mod

    1r0ll|/|4514h schrieb:

    @camper & @me: Wieso eigentlich nicht gleich einen std::vector zurückgeben?

    das habe ich auch gefragt 😉

    1r0ll|/|4514h schrieb:

    camper schrieb:

    das kopieren beim rückgeben per funktion

    Soweit ich weiß können manche Compiler das wegoptimieren.

    nö, nicht vollständig. nehmen wir doch einfach mal einen std::vector und schauen uns das an:

    typedef std::vector<int> v;
    v foo()
    {
        return v( 1000 );
    }
    
    void bar()
    {
        v b;
        // mach irgendwas mit b...
        b = foo();
    }
    

    der compiler darf hier in foo das kopieren von a per RVO unterlassen, er kann aber auf keinen fall das copy-asignment in bar() vermeiden, und hier wir es auf jedenfall zu einem kopieren kommen. im übrigen ist (N)RVO sehr zerbrechlich; man muss aufpassen, um dem compiler diese möglichkeit nicht von vornherein zu verbauen.

    schauen wir uns an, was mit meinem Array pasiert:

    typedef Array<int> v;
    v foo()
    {
        return v( 1000 );
    }
    
    void bar()
    {
        v b;
        // mach irgendwas mit b...
        b = foo();
    }
    

    das return statement in foo() bewirkt zunächst den aufruf von operator ref und dann die benutzung des move-konstruktors Array(Ref) - der compiler darf das nicht wegoptimieren, denn es ist kein copy-ctor. selbiges passiert dann bei der zuweisung in bar(), um der signatur des zuweisungsoperators gerecht zu werden. im endeffekt kommt es zu 2 aufrufen von operator ref, 2 aufrufen des move-ctors, und einem des zuweisungsoperators. in keinem dieser funktionen kommt es zu einem deep copy - und alle sind hinreichend trivial, dass wir annehmen können, dass der optimierer hier noch einiges einsparen kann, da ja nur builtins getauscht werden.

    ach ja, nebenbei - das genze funktioniert nicht mit vc++, wegen der art wie vc++ 8.5.3 absatz 5 interpretiert.

    natürlich kann man das ganze mit smart-pointern ebenfalls lösen (und dann kontrolliert man ganz genau, wann kopiert wird). der nachteil ist aber eben, dass das array dann kein value-typ ist - man kann damit nicht operieren wie mit richtigen containern.


Anmelden zum Antworten