0

I'm new to C++ and am new to using RAII for deleting allocated memory. I wrote this code as a sample of what it would take to automatically allocate and later delete a character array. I know there's a string class out there, but thought I'd start with the older stuff first. Does this sample look correct? And is it the way others would create a string and delete it automatically?

#include <iostream>

using namespace std;

class StringGetter
{
    private:
        char* theString;

    public:
        StringGetter(char* name)
        {
            this->theString = new char[1024];

            strcpy_s(this->theString, strlen(name) + 1, name);
        }

        char* GetString()
        {
            return this->theString;
        }

        ~StringGetter()
        {
            delete[] this->theString;
        }
};

int main()
{
    char* response;

    {
        StringGetter getter("testing");
        response = getter.GetString();
    }

    cin.get(); // pauses the console window
}
NickRamirez
  • 340
  • 4
  • 10
  • 2
    You've missed the Rule of Three. – chris Dec 31 '13 at 20:43
  • 1
    You have used the security enhanced `strcpy` only to create a buffer overrun waiting to happen. You should allocate the appropriate size: `theString = new char[strlen(name)+1];`. The parameter to your c'tor should be `const char*` as well. You are not modifying the parameter. Your `GetString()` method is evil: It returns a modifiable string, essentially giving up control. – IInspectable Dec 31 '13 at 20:47
  • @chris Thanks, I hadn't heard of that before, or if I did I forgot it. – NickRamirez Dec 31 '13 at 20:49
  • @IInspectable That's a good idea too – NickRamirez Dec 31 '13 at 20:50
  • 2
    @IInspectable: Why is that evil? &string::front() also returns the modifiable internal buffer in C++11. – user2345215 Dec 31 '13 at 20:52
  • You are using `strcpy_s` incorrectly -- the length parameter is the length of the destination buffer, not the source buffer. You'd use 1024 here because that's the size you allocated. – Billy ONeal Dec 31 '13 at 20:53
  • Yes, you have the basic idea correct. There are improvements to be made in the implementation details. – TimDave Dec 31 '13 at 20:55
  • @user2345215 `std::string::front()` returns a (modifiable) reference to the **first** character. If you take the address of it and interpret it as the internal buffer you are treading undefined behavior territory. – IInspectable Dec 31 '13 at 21:00
  • @IInspectable If I continue to return a modifiable buffer, is there any risk that the user could do anything that would prevent the memory from being freed? Or is the risk more related to ownership of the data? – NickRamirez Dec 31 '13 at 21:10
  • @IInspectable: That's FALSE. The standard guarantees strings are contiguous which means `&*begin() + i = &operator[i]` – user2345215 Dec 31 '13 at 21:13
  • The danger is in giving up control over your controlled sequence. A caller could remove the `NUL` terminator, for example. The caller cannot do anything that would prevent you from releasing the memory. – IInspectable Dec 31 '13 at 21:13
  • @user2345215 This is not about being contiguous or not. The undefined behavior is a result of **modifying** the controlled sequence. – IInspectable Dec 31 '13 at 21:15
  • @IInspectable: I don't believe that, but I'm not going to argue about it anymore, since this is getting too long. – user2345215 Dec 31 '13 at 21:17
  • 1
    I would start by choosing a name that doesn't mislead. Clarity of purpose is very important. As far as I can see this "StringGetter" is not getting any strings. – Cheers and hth. - Alf Dec 31 '13 at 21:28

2 Answers2

1

It looks like you get the main idea, but there's a couple things worth mentioning.

1) As @chris mentioned, you're forgetting your copy constructor, copy assignment operator, move constructor, and move assignment operator. Copy should be either manually written or disabled, move can be defaulted. (aka You've not followed the rule of 5)

2) Prefer to use std::unique_ptr to represent ownership. It's already done all of the hard work for you. By keeping your wrapped string in a std::unique_ptr the default versions of the copy/move special functions will preserve correctness (though you'll still want to implement the copy operations if you want them to be enabled).

Here's what this might look like:

class StringGetter {
  public:
    explicit StringGetter(char* name) {
      strSize = strlen(name);
      str = std::unique_ptr<char[]>(new char(strSize+1));
      std::copy_n(name, strSize + 1, str.get());
    }

    StringGetter(const StringGetter& other) {
      strSize = other.strSize;
      str = std::unique_ptr<char[]>(new char(strSize+1));
      std::copy_n(other.str.get(), strSize + 1, str.get());
    }

    StringGetter(StringGetter&& other) = default;

    StringGetter& operator=(const StringGetter& rhs) {
      auto temp = rhs;
      swap(temp);
      return *this;
    }

    StringGetter& operator=(StringGetter&& rhs) = default;

    const char* getString() const noexcept {
      return str.get();
    }

    void swap(StringGetter& other) {
      std::swap(str, other.str);
      std::swap(strSize, other.strSize);
    }
  private:
    std::unique_ptr<char[]> str;
    int strSize;
};

Miscellaneous items:

1) Note that std::unique_ptr handles the RAII. When I replace 'str' in the copy constructor, it deletes the old string automatically.

2) I size the dynamically allocated memory to match the input string. This prevents overflows/wasted memory.

3) The constructor is explicit. This prevents undesirable conversions. Rule of thumb is to use the explicit keyword on all single argument constructors.

4) I made getString constant so that you can still call it on const instances of the class.

5) I replaced the str-family copy function with std::copy_n. It's more general and can avoid certain pitfalls in obscure cases.

6) I used the copy-swap idiom in the copy assignment operator. This promotes code reuse/avoids duplication.

7) When C++14 comes out, replace the std::unique_ptr constructor call with std::make_unique for added exception-safety and to remove redundancy.

Mark
  • 3,806
  • 3
  • 21
  • 32
  • Is the copy assignment operator safe when applied to self assignment? – IInspectable Dec 31 '13 at 21:30
  • @IInspectable - Should be, yes. You first make a copy with the copy constructor, so you never even have the same instance on both sides of an equals sign. Look up the copy-swap idiom. If you expected to be doing self assignment frequently, I'd advise adding an if statement to check for that so that you can return early and avoid the O(n) work. – Mark Dec 31 '13 at 21:37
  • 1
    @IInspectable - you may find [this](http://www.umich.edu/~eecs381/lecture/Objectdynmemory.pdf) to be a helpful resource (page 10). The idiom actually buys us exception safety as well. – Mark Dec 31 '13 at 21:40
  • Copy assignment operator should be `StringGetter& operator=(StringGetter rhs)`, yes? – Brandon Dec 31 '13 at 22:42
  • @CantChooseUsernames - That is one valid form, yes, so is the const & form I used. If you wanted to use that function signature, you can get rid of the `auto temp = rhs;` line, as you will have already made a copy. If you do not use the copy-swap idiom, I highly recommend using the const & version (to avoid an accidental copy). – Mark Dec 31 '13 at 22:45
  • I understand. I only knew it as the signature I wrote because I always use it and it has the advantages this guy pointed out: http://stackoverflow.com/questions/3279543/what-is-the-copy-and-swap-idiom I realise some use the signature you use. Just never saw it like that before. – Brandon Dec 31 '13 at 22:51
0

Here is an attempt at an RAII class that does something similar:

template<std::size_t N>
class StringGetter_N
{
private:
  std::unique_ptr<char[]> buffer;

public:
    StringGetter_N()
    {
      buffer.reset( new char[N] );
      buffer.get()[0] = 0;
    }
    explicit StringGetter_N(char const* name)
    {
      buffer.reset( new char[N] );
      strcpy_s(buffer.get(), N-1, name);
      buffer.get()[N-1] = 0; // always null terminate
    }
    StringGetter_N( StringGetter_N&& other ) = default;

    char* GetString()
    {
        return buffer.get();
    }
};
class StringGetter : StringGetter_N<1024> {
  explicit StringGetter( const char* name ):StringGetter_N<1024>(name) {}
  StringGetter():StringGetter_N<1024>() {}
};

notice that I delegated the resource management to a smart pointer. Single responsibility principle means that the resource management object (like a smart pointer) can do just that, and if you want to write a class that represents a heap-allocated buffer of fixed size, you delegate that sub problem to the smart pointer, and just manage it in your class.

As it happens, std::unique_ptr properly implements

But really, it is usually much simpler to just use a std::vector, as you can usually determine how much space you need at run-time before needing a buffer to write to.

If you do implement your own RAII class, you should follow these rules:

  • Single argument constructors that are not copy/move constructors should usually be explicit.
  • If you implement a non-trivial destructor, you must (implement or block usage of) (copy and/or move constructor) and (copy and/or move operator=). This is known as the rule of 3 (or rule of 5 in C++11).
  • Your RAII class should do little except manage the resource. If you want to do something else, use an RAII class to manage the resource, then store it within another class that does the fancy extra work.
Yakk - Adam Nevraumont
  • 262,606
  • 27
  • 330
  • 524