简体   繁体   中英

How to best handle copy-swap idiom with uninitialised memory

As an academic exercise I created a custom vector implementation I'd like to support copying of non-pod types.

I would like the container to support storing elements that do not provide a default constructor.

When I reserve memory for the vector, and then push_back an element (which manages it's own resources and has a copy and assignment operator implemented - I'm ignoring move constructors for the moment) I have an issue using the copy-swap idiom for that type.

Because the swap happens on a type that is still uninitialised memory, after the swap, the destructor which is called for the temporary will attempt to free some piece of uninitialised data which of course blows up.

There are a few possible solutions I can see. One is ensure all non-pod types implement a default constructor and call that (placement new) on each element in the collection. I'm not a fan of this idea as it seems both wasteful and cumbersome.

Another is to memset the memory for the space of the type in the container to 0 before doing the swap (that way the temporary will be null and calling the destructor will operate without error). This feels kind of hacky to me though and I'm not sure if there is a better alternative (see the code below for an example of this) You could also memset all the reserved space to 0 after calling reserve for a bunch of elements but again this could be wasteful.

Is there documentation on how this is implemented for std::vector as calling reserve will not call the constructor for allocated elements, whereas resize will (and for types not implementing a default constructor a constructed temporary can be passed as a second parameter to the call)

Below is some code you can run to demonstrate the problem, I've omitted the actual vector code but the principle remains the same.

#include <iostream>
#include <cstring>

// Dumb example type - not something to ever use
class CustomType {
public:
    CustomType(const char* info) {
        size_t len = strlen(info) + 1;
        info_ = new char[len];
        for (int i = 0; i < len; ++i) {
            info_[i] = info[i];
        }
    }

    CustomType(const CustomType& customType) {
        size_t len = strlen(customType.info_) + 1;
        info_ = new char[len];
        for (int i = 0; i < len; ++i) {
            info_[i] = customType.info_[i];
        }
    }

    CustomType& operator=(CustomType customType) {
        swap(*this, customType);
        return *this;
    }

    void swap(CustomType& lhs, CustomType& rhs) {
        std::swap(lhs.info_, rhs.info_);
    }

    ~CustomType() {
        delete[] info_;
    }

    char* info_;
};

int main() {
    CustomType customTypeToCopy("Test");

    // Mimics one element in the array - uninitialised memory
    char* mem = (char*)malloc(sizeof(CustomType));

    // Cast to correct type (would be T for array element)
    CustomType* customType = (CustomType*)mem;  
    // If memory is cleared, delete[] of null has no effect - all good
    memset(mem, 0, sizeof(CustomType));
    // If the above line is commented out, you get malloc error - pointer 
    // being freed, was not allocated

    // Invokes assignment operator and copy/swap idiom
    *customType = customTypeToCopy;

    printf("%s\n", customType->info_);
    printf("%s\n", customTypeToCopy.info_);

    return 0;
}

Any information/advice would be greatly appreciated!

Solved!

Thank you to @Brian and @Nim for helping me understand the use case for when assignment (copy/swap) is valid.

To achieve what I wanted I simply needed to replace the line

*customType = customTypeToCopy;

with

new (customType) CustomType(customTypeToCopy);

Invoking the copy constructor not the assignment operator!

Thanks!

You don't use copy-and-swap for construction.

You use copy-and-swap for assignment in order to solve the following problem: the left side of the assignment is an already-initialized object, so it needs to free the resources it holds before having the right side's state copied or moved into it; but if the copy or move construction fails by throwing an exception, we want to keep the original state.

If you're doing construction rather than assignment---because the target is uninitialized---the problem solved by copy-and-swap doesn't exist. You just invoke the constructor with placement new. If it succeeds, great. If it fails by throwing an exception, the language guarantees that any subobjects already constructed are destroyed, and you just let the exception propagate upward; in the failure case the state of the target will be the same as it was before: uninitialized.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM