Why use std::vector?

Started by
34 comments, last by ToohrVyk 16 years, 8 months ago
Quote:Original post by SiCrane
Previous thread on why memcpy() isn't a viable option for UDTs.


Right; let me rephrase my original question: IF you have a vector of non-derived classes, or a fixed derivation of a class (does sizeof() include virtual function tables? You may just have me there), then is there any benefit to that behavior? Yes, I know that that's a big if; but if I'm storing objects and not their pointers anyway, it's a safe bet I'm not using polymorphism. I understand perfectly why that doesn't work.

Quote: Change A() to std::string("Hey we have a long string!!!") and you'll find out, in the form of a crash. As soon as the type you are storing in the vector has a pointer to dynamic memory, you're going to run into problems when using memcpy. That's why standard containers use copy constructors and such, to ensure they work with every type(provided they implement a proper copy constructor, etc)


Nope. There is no destructor called, yet the old pointer is de-referenced. The pointer passes hands safely since neither constructor nor destructor is called, but the data (pointer address) is copied precisely. Try it.

As for ToohrVyk . . .
Quote: memcpy is a function which copies a block of bytes from one location in memory to another. It doesn't copy PODs, it doesn't copy integers, it doesn't copy non-PODs, the only thing it can copy is bytes.

This I know, though thank you for clarifying politely. It is the intended behavior.

As far as self-referencing objects that register themselves, yes, I thought of this. And yes, instead of a memcpy based vector you'd need to use std::vector (or perhaps a linked list, personally, since they're registering themselves elsewhere anyway).

-Walt
Advertisement
Quote:Original post by shadowman131
This I know, though thank you for clarifying politely. It is the intended behavior.


In that case, what did you intend to copy with it? You do have an array of non-POD objects, but you have no bytes. Or, if you did manage to copy an array of bytes to another, how would you transform the array of bytes back into an array of non-POD objects?

Quote:does sizeof() include virtual function tables? You may just have me there


It's a little bit worse than that: there is no such thing as a virtual function table in C++—so wondering whether sizeof() includes it or not is a moot point. A virtual table point doesn't exist in C++ either.

Some compilers may, when certain compiling options are used, implement virtual functions using a pointer to a table of function pointers. Sometimes, that pointer will appear as part of the value returned by sizeof.

However, a compiler is free to implement virtual functions through a non-intrusive method (ranging from hashing tables to placing a virtual pointer outside the actual class memory to whatever magic they may come up with).
Nope what? memcpying 2 dynamic strings is not safe in the least, barring such hacks as the guy in the other thread is doing, and not calling the destructor, which is a horrible hack to get around something which has trivial alternatives.

Sounds like your 'idea' is operating on raw memory as if they were objects. Also not recommended.
Quote:Original post by ToohrVyk
Quote:Original post by shadowman131
This I know, though thank you for clarifying politely. It is the intended behavior.


In that case, what did you intend to copy with it? You do have an array of non-POD objects, but you have no bytes...


Even if the standard doesn't like it (yes, I'm stepping on slightly dangerous ground) the non-POD type sure is an array of bytes. Perhaps the format of them is undefined, but there are definitely valid bytes in it, particularly (as I am interested in) if all of its members are fixed-size classes or primatives.

-Walt
Quote:Original post by DrEvil
Nope what? memcpying 2 dynamic strings is not safe in the least, barring such hacks as the guy in the other thread is doing, and not calling the destructor, which is a horrible hack to get around something which has trivial alternatives.

Sounds like your 'idea' is operating on raw memory as if they were objects. Also not recommended.


I suggest you step through the memcpy process. It does not copy the string, but it does call the destructor and constructor properly (and once per allocated string).

As far as raw memory; it's working on the clearly defined (sizeof()) chunks of memory. It makes no assumption as to the content of memory, only the definition of pointers (a single address referring to a multiple (usually defined) number of bytes).
Quote:
I suggest you step through the memcpy process. It does not copy the string, but it does call the destructor and constructor properly

I'm assuming you just worded this poorly? memcpy() will not call constructors or destructors at all. The overall process of having, say, a custom_vector<std::string> and internally using memcpy() to move the storage might result in the correct number of constructor/destructor calls, but that still doesn't make it correct or legal to do so.
Quote:Original post by shadowman131
I suggest you step through the memcpy process. It does not copy the string, but it does call the destructor and constructor properly (and once per allocated string).


Since when does memcpy allocate anything? Since when does it call destructors and constructors? (hint: it does no such thing)

Quote:As far as raw memory; it's working on the clearly defined (sizeof()) chunks of memory. It makes no assumption as to the content of memory, only the definition of pointers (a single address referring to a multiple (usually defined) number of bytes).


memcpy works on the size that you pass it. Sounds to me like we're talking about 2 different things, care to clarify? I'm talking about the memcpy function which takes 2 addresses and a size and does a byte for byte copy. What are you talking about that that supposedly calls constructors and destructors?
Quote:Original post by shadowman131
Perhaps the format of them is undefined, but there are definitely valid bytes in it, particularly (as I am interested in) if all of its members are fixed-size classes or primatives.


Yes, these are valid bytes, which may be read by the program. The problem, however, is the reverse transformation. So (now discussing at the machine level, not the C++ level) you copy the bytes found at the object's memory location to another location. Why would that be sufficient to generate a fully working C++ object at the new location? The compiler is free to put object state information outside the object (and most compilers do, for instance in the form of a virtual table), so you will need to find a way to locate and alter the external representation so that the object works at the new location—this is assuming that you have write-access to that representation in the first place—and will involve a lot of reading compiler documentation (in those case where the behaviour is actually documented at all).

In short, your array class would probably have a nice label indicating "this class is only guaranteed to work on version Y of compiler X with options Z—use at your own risk on another compiler" which may or may not be acceptable to you.

Of course, that's ignoring the fact that explicitly invoking undefined behaviour is akin to a lemming run, if you don't have complete control over the compiler—that is, if you wrote the compiler, you may invoke undefined behaviour when you know what'll happen, otherwise you can't expect anything...
*sigh* well, personally I think better in code. I urge you all to compile/look at this and see what I mean. I'm sure it does a better job explaining than me.

Oh, as far as derived classes - you're quite right on that, it's not safe to copy any sort of type information. So, from here on, let's just assume that the templated class T is not derived. Ok? There are still plenty of non-POD (maybe I'm mistaken about the definition of POD) classes that are not derived.

#include <malloc.h>template<typename T>class custom_vector{public:    /**Constructor.  Does nothing but set the storage to uninitialized.      */    custom_vector() : numObjects(0), maxObjects(0), storage(0) {}    /**Destruction.  Calls freeStorage().      */    ~custom_vector()     {        _freeStorage();    }    /**In case the vector is, for instance, a pointer type, it may be easier      *to use a member function to access elements.      * @param index Specifies which element to return.      * @return Returns the requested array element (non-constant).      */    T& at(size_t index)    {        return *((T*)storage + index);    }    /**In case the vector is, for instance, a pointer type, it may be easier      *to use a member function to access elements.      * @param index Specifies which element to return.      * @return Returns the requested array element (constant).      */    const T& at(size_t index) const    {        return *((T*)storage + index);    }    /** @return Returns the requested array element (non-constant).      */    T& operator[](size_t index)    {        return *((T*)storage + index);    }    /** @return Returns the requested array element (constant).      */    const T& operator[](size_t index) const    {        return *((T*)storage + index);    }    /** @return Reports the number of objects in this vector.      */    size_t size() const    {        return numObjects;    }    /**Adds a new element to the end of the list.      *Note that this invokes a copy constructor.      * @param object Object to add.      */    void push_back(const T& object)    {        numObjects++;        _checkStorage();        new((T*)storage + numObjects - 1) T(object);    }    /**Without parameters, push_back adds a default construction of the object.      *This avoids a copy constructor, assuming default construction parameters      *are acceptable.      */    void push_back()    {        numObjects++;        _checkStorage();        new((T*)storage + numObjects - 1) T();    }    /**Removes and deletes the final element in storage.      */    void pop_back()    {        numObjects--;        ((T*)storage + numObjects)->~T();    }    /**Exchanges two elements.  This function is provided to help avoid copy       *constructors (for a temporary object).      * @param element1 First element to be exchanged      * @param element2 Element to exchange with the first      */    void exchange(size_t element1, size_t element2)    {        char exchangeSpace[sizeof(T)];        T* element1Ptr = (T*)storage + element1;        T* element2Ptr = (T*)storage + element2;        memcpy(exchangeSpace, element1Ptr, sizeof(T));        memcpy(element1Ptr, element2Ptr, sizeof(T));        memcpy(element2Ptr, exchangeSpace, sizeof(T));    }    /**Adjusts the storage capacity to the requested size.  If smaller than the      *current array, objects will be deleted off of the end down to the new       *size.  If larger than existing array, new (default) objects will be       *created to fill the new space.      */    void resize(size_t newSize)    {        if (numObjects == newSize)            return;        else if (numObjects > newSize) {            T* freeObject = (T*)storage + numObjects - 1;            while (numObjects > newSize) {                freeObject->~T();                freeObject--;                numObjects--;            }            _resizeStorage(newSize);        }        else { //numObjects < newSize            _resizeStorage(newSize);            T* newObject = (T*)storage + numObjects;            while (numObjects < newSize) {                new(newObject++) T();                numObjects++;            }        }    }private:    /**Asserts that storage is large enough to contain all of our objects.        *Note that functions adding to storage size call this after incrementing      *numObjects.      */    void _checkStorage()    {        if (numObjects > maxObjects) {            if (!storage) {                const size_t growth = 10;                storage = (char*)malloc(sizeof(T) * growth);                maxObjects = growth;            }            else {                const size_t growth = numObjects + (numObjects >> 1) + 10;                storage = (char*)realloc(storage,                   sizeof(T) * growth);                maxObjects = growth;            }        }    }    /**Resizes the storage array without touching the object count.      *Does update maxObjects, however.      * @param count The new size of the storage array.      */    void _resizeStorage(size_t count)    {        if (storage) {            storage = (char*)realloc(storage, sizeof(T) * count);        }        else            storage = (char*)malloc(sizeof(T) * count);        maxObjects = count;    }    /**Frees the storage array.      */    void _freeStorage()    {        for (size_t i = 0; i < numObjects; i++) {           ((T*)storage + i)->~T();        }        free(storage);    }    //Our actual data storage.  Stored as a char* for easy addressing.    char* storage;    //Size of the storage    size_t numObjects;    //Maximum size of the storage    size_t maxObjects;};


Then, for our std::string arguers:
#include <string>void main(){    custom_vector<std::string> myVec;    for (int i = 0; i < 10000; i++) {        myVec.push_back(std::vector("Hello bob. . . "));    }    //put a break point if you want; they're all valid, separate    //strings.  Only 10000 constructors were called, and 10000     //copy constructors.}


Oh, and for a separate issue . . . is using size_t bad? I think it is, but not sure on that one. I grabbed it because it's well defined as the native unsigned integer type, unless, again, I'm mistaken.

-Walt
Well for one thing, your class isn't exception safe. For another thing, why write all that yourself when you can just use a standard library implementation that allows you to specify type traits for classes you know have trivial copy and assign? e.g.: STLPort with it's __type_traits class.

This topic is closed to new replies.

Advertisement