6

Imagine the following class:

class MyString {
public:
    const char* str;
    std::size_t str_len;

    MyString(const char* str, std::size_t str_len)
        : str { str }
        , str_len { str_len }
    {}
}

I'm a bit confused about implementing a destructor for MyString. My first thought was that it would look like this:

~MyString() {
    delete [] str;
}

But how can I delete str if I can't be sure that it was allocated? For example, I could create an instance of MyString like this:

const char* c_string = "Hello, World!";
MyString my_string(c_string, 13);

in which case I shouldn't delete str because it was not declared on the heap, but if I created an instance of MyString like this:

char* char_array = new char[13]{'H','e','l','l','o',',',' ','W','o','r','l','d','!'};
MyString my_string(char_array, 13);

not deleting str would cause a memory leak (I assume) because it would be declared on the heap. But if I created an instance of MyString like this:

char* char_array = new char[13]{'H','e','l','l','o',',',' ','W','o','r','l','d','!'};
MyString my_string(char_array + 3, 10);

I shouldn't delete str because although it's on the heap, it hasn't been allocated; it just points to part of something else that's been allocated.

So how can I be sure that I'm not deleting something that I shouldn't be deleting or failing to delete something that needs to be deleted? Would the answer be different if MyString used char*s instead of const char*s? What if I used MyString my_string = new MyString...?

Edit: To clarify, I'm not actually writing a string class. I'm using a char array as a byte array. I assume std::string wouldn't work, since the bytes could be 0.

EKW
  • 2,059
  • 14
  • 24
  • 4
    Use std::string. It'll always make a copy, and clean up after itself. – Mikel F Dec 31 '16 at 05:19
  • 3
    Have your class copy the string passed in every time, everything else is probably just bad design. Also use a `std::string`. – user657267 Dec 31 '16 at 05:19
  • Every `new` should be matched with corresponding `delete`. Every `new T[]` should be matched with corresponding `delete []`. Nowdays you should be using smart pointers instead. –  Dec 31 '16 at 05:22
  • Don't forget that the `std::string` takes into account the 'end of string' character (i.e. `\0`) .. So `"Hello World!"` is actually `char[]{'H','e','l','l','o',' ','W','o','r','l','d','!','\0'}` – txtechhelp Dec 31 '16 at 05:24
  • @txtechhelp: The string class in the question is counted, not delimited. Counted strings are completely viable and while they aren't very popular in C or C++, other platforms use them extensively. – Ben Voigt Dec 31 '16 at 05:26
  • @BenVoigt .. agreed .. but a lot of mention of `std::string` is made and if OP is trying to emulate it's behavior they should be aware of this – txtechhelp Dec 31 '16 at 05:27
  • If you're trying to use a `char*` as a type of byte array, why not use a `std::vector` or some other sequence container? – txtechhelp Dec 31 '16 at 05:34
  • @txtechhelp I should have specified, what I'm doing isn't actually accepting a string, but rather an array of bytes. Because some of those bytes might be 0, I have to store the array's length rather than iterating until the first 0-byte – EKW Dec 31 '16 at 05:35
  • 3
    "std::string wouldn't work, since the bytes could be 0.". `std::string` works fine with 0-bytes. – Cheers and hth. - Alf Dec 31 '16 at 05:40
  • @Cheersandhth.-Alf I always assumed that std::string figured out its length by searching for the first 0-byte. Is that wrong? – EKW Dec 31 '16 at 05:44
  • 1
    Yes, that's wrong. However, it can figure out the length of a C string that way. Representing the byte array as a `std::string` is fine, but passing it around as a zero-terminated C string is ungood. – Cheers and hth. - Alf Dec 31 '16 at 05:48
  • You could use a sequence container like so: `class MyString { private: std::vector m_vec; public: MyString(char* arr, std::size_t sz) : m_vec(arr, arr+sz) { } };` .. – txtechhelp Dec 31 '16 at 05:54
  • Seems to me you want `string` when the object owns the content (and will delete it), and `string_view` if it won't own it. – Jerry Coffin Dec 31 '16 at 07:19

3 Answers3

10

There are a couple different patterns that apply:

  1. The always-allocate pattern. In this approach, the class doesn't take ownership of the passed-in resource, rather it makes a copy in a buffer it allocated and therefore knows how to deallocate in its destructor. The original parameter is owned by the code calling the class, and that caller should clean up its own data whenever it wants, because the class instance has an independent copy. Example: std::string.

  2. The caller-specified-deleter pattern. In this approach, the class does take ownership, and to accomodate a variety of allocator/deallocator pairs, it accepts a parameter which is a function or function object that knows how to deallocate the data. The class destructor will call this deleter function/function object, performing the correct deallocation (or none at all) needed for that particular buffer. Example: std::shared_ptr.

  3. The nested-ownership pattern. Here, the class just keeps a pointer or reference to the original block of data. The caller still has ownership and the responsibility to free the data, but it additionally is required to keep that block valid for as long as the class instance it created exists. This is the lowest overhead at runtime, but also the most difficult to keep track of. Example: by-reference variable capture in a C++11 lambda.

Whichever of these you use for your class design, make sure you document it so the users of your class aren't left wondering.

Ben Voigt
  • 277,958
  • 43
  • 419
  • 720
1

But how can I delete str if I can't be sure that it was allocated?

You can delete str only if:

  1. You document that you will be taking ownership of the pointer being passed to the constructor.

  2. You document that you will be calling delete on the memory passed to the constructor.

  3. Construct instances of the class only with memory allocated by call to new.

I would also recommend changing the class to use char* instead of const char*.

class MyString {
public:
    char* str;
    std::size_t str_len;

    MyString(char* str, std::size_t str_len)
        : str { str }
        , str_len { str_len }
    {}
}

That would prevent accidental usages like:

MyString my_string("Hello, World!", 13);

And then, you have to make sure that you follow The Rule of Three.

Community
  • 1
  • 1
R Sahu
  • 204,454
  • 14
  • 159
  • 270
1

To clarify interface, you may use appropriate smart pointer, as for example:

MyString(std::unique_ptr<const char[]> str, std::size_t str_len)

or if you don't take ownership, appropriate string-view, as for example:

MyString(std::experimental::observer_ptr<const char> str, std::size_t str_len)

Then you no longer doubt on memory policy of the class.

Jarod42
  • 203,559
  • 14
  • 181
  • 302