2
\$\begingroup\$

Following is my simple implementation of the std string class. Please review my code and offer some advice. I want to figure out is there anything wrong with my code or is there anything I can improve on my code.

#include <iostream>
using namespace std;
class String{
private:
    char *buf=nullptr;
    int size;
public:
    String();
    String(const char* str);
    String(const String& str);
    String(String&& str);
    int length() const;
    String& operator =(const String& str);
    String& operator =(String&& str);
    char& operator [](int pos);
    String& operator +(const String& str);
    ~String();
};

String::String() {
    buf=nullptr;
    size=0;
}

String::String(const char* str) {
    if(str==nullptr) {
       str=new char[1];
       str="\0";
    }
    else {
        int i=0;
        const char* p=str;
        while(*p++) i++;
        buf=new char[i];
        int j=0;
        for(j;*str;str++,j++) buf[j]=*str;
        size=j;
    }
}

String::String(const String &str) {
    size=str.size;
    buf=new char[size];
    for(int i=0;i<size;i++) buf[i]=str.buf[i];
}

String::String(String&& str) {
    buf=str.buf;
    size=str.size;
    str.buf=nullptr;
}

int String::length() const {
    return size;
}

String& String::operator =(const String& str) {
    if(this!=&str) {
        if(buf) delete []buf;
        size=str.size;
        buf=new char[size];
        for(int i=0;i<size;i++) buf[i]=str.buf[i];
    }
}

String& String::operator =(String&& str)
{
    if(this!=&str){
        delete []buf;
        size=str.size;
        buf=str.buf;
        str.buf=nullptr;
        str.size=0;
    }

    return *this;
}

String::~String()
{
    delete []buf;
}

char& String::operator [](int pos)
{
    return buf[pos];
}

String& String::operator +(const String& str)
{
   int newsize=size+str.length();
   char* p=new char[newsize];
   int j=0;
   for(int i=0;i<size;i++) p[j++]=buf[i];
   for(int i=0;i<str.length();i++) p[j++]=buf[i];
   String *s=new String(p);
   return *s;
}

int main()
{
    String s1;
    cout<<s1.length()<<endl;
    char a[2]={'a','b'};
    String s2(a);
    cout<<s2.length()<<endl;
    string s3(a);
    cout<<s3.length()<<endl;
    String s4(s2);
    cout<<s4.length()<<endl;
    String s5(String("abc"));
    cout<<s5.length()<<endl;
    String s6=s5;
    cout<<s6.length()<<endl;
    String s7=String("abcd");
    cout<<s7.length()<<endl;
    char x=s7[0];
    cout<<x<<endl;
    String s8=s7+s6;
    cout<<s8.length()<<endl;
}

 
\$\endgroup\$

1 Answer 1

4
\$\begingroup\$

Overview

The main issue is to decide if the stored char buffer is '\0' terminated or not. Make it clear in comments then write tests to check for it. The first constructor does this explicitly but other constructors don't seem to (not sure if this is accident or on purpose so make it clear with comments).

Move operations. The src of the move must be in a valid state. I assume this means a nullptr in buf requires a zero size.

Move operations in general should be noexcept unless you really can't do it.

You did not use the Copy and Swap idiom. This is very standard so I would advise going to learn about it.

And one bug in the operator+().

Note:

 char a[2] = {'a', 'b'}; // This is not a null terminated string.
                         // The compiler is not adding the '\0'

                         // You can do this manually
                         // {'a', 'b', '\0'}
                         // But using quotes is easier.

                         // Also being explicit with the size 2
                         // is a smell as you might count wrong.
                         // If you leave the brackets empty the compiler
                         // will do the counting for you.

 // Use this:
 char a[] = "ab";        // Create an array three characters long.
                         // a[0] = 'a'
                         // a[0] = 'b'
                         // a[0] = '\0'
  

Design

Your implementation only supports a size (not size and capacity). You will find that many dynamically resizeable objects contain a current size and a "maxSize" that an object can be expanded up to before reallocation.

You want to do this because memory allocation is relatively expensive so you want to minimize the number of times you do this. So you can initially allocate more than you need and then incrementally expand the object (without further dynamic allocation) until you reach the maxSize then reallocate more space.


Types are important in C++.

If you are storing size you should use std::size_t which is designed for sizes. So prefer this. For times when you want to optimize to a more efficient type, make those arguments in the comments.

Next Steps

In the standard library, strings are "Containers". There are a couple of extra things you need to do to make your class a Container.

But the first things you should do are create iterators and the begin and end methods (in all their variations - have a look at std::string).

When your class is a container you can then use range-based for:

String   data("This is a string");
for (auto const& c: data) {
    std::cout << "Char: " << c << "\n";
} 

Code Review

Don't do this:

using namespace std;

Especially in a header file.

see: Why is "using namespace std;" considered bad practice?


You did not have your code in its own namespace.

Especially with classes that have been written before it is important to isolate your code to make sure the compiler and other users don't confuse them.


Would you not want the default constructor to hold an 'empty' string?

String::String() {
    buf=nullptr;
    size=0;
}

This would match the behavior of the constructor that could take a null str parameter (see next constructor).

Also a lot of code becomes less complicated if you don't allow the buffer to be nullptr. Then you don't need to check for null before you use it etc..


When writing constructor, prefer to use the initializer list. This is because if you don't the members will be default initialized (for POD this means nothing happens) but in most cases it means you will be doing extra work to default initialize the member then reset it in the body. So do it once in the initializer list.

String::String()
    : buf(nullptr)
    , size(0)
{}

Just make it the default way you initialize members.


This constructor:

String::String(const char* str) {

In this half you don't set the size member:

    if(str==nullptr) {
       str=new char[1];
       str="\0";
    }

First: There are already good (well optimized functions) to handle C strings. You should use those rather than write your own.

It looks like you don't allocate enough space (I think you are one short for the \0 to mark the end of the string, as you did above).

Step 1: Calculate Length:

        int i=0;
        const char* p=str;
        while(*p++) i++;

        // Better would be to use for loop then subtract pointers
        // to get length.

        char const* end;
        for (end = str; *end; ++end) {/* No Action */}
        std::size_t size = (end - str) + 1;  // One for the '\0'

        // Best use C functions
        std::size_t size = strlen(str) + 1;

Step 2: Allocate memory

        buf=new char[i];  // But not long enough. Should by i + 1

        // Sure nothing wrong here.

Step 3: Copy String

        int j=0;
        for(j;*str;str++,j++) buf[j]=*str;

        // You can declare variables inside the for statement.
        for (char* dst = buf; *src; ++dst, ++src) {
            *dst = *src;
        }
        *dst = '\0';

        // But better still use C function.
        strcpy(buf, src);

Step 4: Set Size:

        // Why do you have a separate value?
        // You could have used `size` above rather than introduce `j`.
        size=j;

Again you forgot to copy the \0 at the end.

Again I assume you want the string to be null-terminated because you set it in the default constructor. But no other place.

String::String(const String &str) {
    size=str.size;
    buf=new char[size];
    for(int i=0;i<size;i++) buf[i]=str.buf[i];
}

This is broken:

String::String(String&& str) {
    buf=str.buf;
    size=str.size;
    str.buf=nullptr;
}

The str value has to be put into a valid state. After you have moved the value, that state is not valid (the size has the old value but the buffer is null). The state needs to be valid.

Normally, you want to make sure that move operators are noexcept. A lot of algorithms are optimized to use move operators IF they are noexcept. But these algorithms MUST fallback to copy operations if the move operator is not exception safe as otherwise they can not provide the Strong exception guarantee.

Normally the simplest way to do this is to set a default state (like the default constructor) then swap the state of the two objects (as swap is exception safe).

 String::String(String&& str) noexcept
     : buf(nullptr)               // If you want to force null termination
     , size(0)                    // I leave that as an exercise.
 {
     swap(str);
 }

 void String::swap(String& other) noexcept
 {
     std::swap(size, other.size);
     std::swap(buf,  other.buf);
 }
 friend void swap(String& lhs, String& rhs) {
     lhs.swap(rhs);
 }

This is useful because the swap function is used by a lot of algorithms in the standard. And it can be used by the assignment operator.


Looks good.

int String::length() const {
    return size;
}

This is not the best implementation.

String& String::operator =(const String& str) {
    if(this!=&str) {
        if(buf) delete []buf;   // Why not try and re-use?
        size=str.size;
        buf=new char[size];
        for(int i=0;i<size;i++) buf[i]=str.buf[i];
    }
}

String& String::operator =(String&& str)
{
    if(this!=&str){
        delete []buf;
        size=str.size;
        buf=str.buf;
        str.buf=nullptr;
        str.size=0;
    }

    return *this;
}

First issue that checking for self assignment is a pessimization (the extra if branch is not optimal). Yes you do have to account for self assignment. BUT this is exceedingly rare and thus optimizing for the normal case and occasionally taking a hit for self assignment is the better pattern.

Note: This is the normal logic there are situation where this does not hold. But these are by far the exception and in those specific cases you will do a check for self.

But the "standard way to implement this is to use the "Copy and Swap" idiom.

String& String::operator=(String str) noexcept
{
    swap(str);
    return *this;
}

You will notice that str is passed by value. You then swap this parameter with the current state and return a reference to self.

This works for both the copy and move operations.

  • If this is a copy assignment. Then the rhs value is copied into str (thus you get a copy like above). You then swap the state of the copied parameter with the state of the current object. Then when the function exits the parameter is destroyed (thus releasing the original buffer).

  • If this is a copy assignment. Then the rhs value is moved into str (thus you get a move like above). You then swap the state of the moved parameter with the state of the current object. Then when the function exits the parameter is destroyed (thus releasing the original buffer).

So:

String a("Plop1");
String b("Plop2");

a = b;           // Copy assignment.
                 // Because you have a copy constructor.
                 // b copied into `str`.
                 // a and str swapped.
                 // str destroyed (releasing the original value of a)

a = std::move(b);// Move assignment.
                 // Because you have a move constructor.
                 // b moved into `str`.
                 // a and str swapped.
                 // str destroyed (releasing the original value of a)

Yes.

String::~String()
{
    delete []buf;
}

Good if you want to update the string.

char& String::operator [](int pos)
{
    return buf[pos];
}

But what if you passed the String as a const reference into a function (this happens a lot because of const correctness).

void debugInfo(String const& str)
{
    std::cout << "Size: " << str.length() << " F: " << str[0] << "\n"
}

// Fails to compile because str[0] is not a const operation.

So modifiers like this usually have two versions. A normal version that allows mutations and a const version that allows read operations.

char&        String::operator [](int pos);
char const&  String::operator [](int pos) const;

Owwwwwww. That's broken.

String& String::operator +(const String& str)
{
   // Stuff.


   // This string is dynamically allocated with `new`
   String *s=new String(p);

   // So it must be released with a call to `delete`
   // But you are not returning a pointer so the user
   // can not release it.

   // returning a reference to a pointer.
   // This is leaking.
   return *s;
}

Here you can not return by reference. You have to return by value.

String String::operator +(const String& str)
{
     // Stuff : create p

     String result(p);
     return result;
}

But normally you would implement operator+ in terms of the += operator. So I would actually do this:

String& String::operator+=(const String& str)
{
     // This could be much more efficient if you 
     // had the concept of capacity (MaxSize) as you could
     // preallocate all the space for both during construction
     // of a temporary.

     // Create new Value
     int    newSize = size + str.size;
     char*  newBuf  = new char[newSize + 1];
     strcpy(newBuf, buf);
     strcat(newBuf, str.buf);

     // Swap state.
     swap(size, newSize);
     swap(buf,  newBuf);

     // Release old buffer
     delete [] newBuf;
     return *this;
}
String String::operator+(const String& str) const
{
     // This could be much more efficient if you 
     // had the concept of capacity (MaxSize) as you could
     // preallocate all the space for both during the copy.
     // so not need a reallocation.
     String  copy(*this);   // copy the current string.
     copy += str;
     return copy;
}

Example:

#include <iostream>

namespace ThorsAnvil
{
class String
{
    private:
        std::size_t capacity;   // Allocated space (all of it, including space for null)
        std::size_t size;       // Character in string (not null)
        char*       buf;        // null terminated string.
    public:
        // Creation / Destruction
        String(char const* str = nullptr);
        String(String const& str);
        String(String&& str) noexcept;
        ~String();

        // Use Copy and Swap idiom.
        // Handles both copy and move assignment.
        String& operator=(String str) noexcept;

        // Standard Func
        void swap(String& other) noexcept;

        // Trivial one liners put in the header.
        std::ostream& print(std::ostream& stream)   const   {return stream << buf;}
        std::size_t length()                        const   {return size;}
        std::size_t maxLenght()                     const   {return capacity;}
        char&       operator[](std::size_t pos)             {return buf[pos];}
        char const& operator[](std::size_t pos)     const   {return buf[pos];}

        // Manipulators
        String& operator+=(String const& str);
        String  operator+(String const& str) const;

        // Friends
        friend void swap(String& lhs, String& rhs)                              {lhs.swap(rhs);}
        friend std::ostream& operator<<(std::ostream& str, String const& data)  {return data.print(str);}

    private:
        String(String const& str, std::size_t capacity);
        static constexpr std::size_t defaultMinCapacity = 10;
        static std::size_t getCapacity(char const* str);
        static void copyString(char const* src, char* dst);
};

std::size_t String::getCapacity(char const* str)
{
    return str == nullptr
        ? defaultMinCapacity
        : std::max(defaultMinCapacity, (strlen(str) * 2) + 1);
}
void String::copyString(char const* src, char* dst)
{
    // compensate for src being null
    char const* dataSrc = src == nullptr ? "" : src;
    strcpy(dst, dataSrc);
}

String::String(const char* str)
    : capacity(getCapacity(str))
    , size(0)
    , buf(new char[capacity]{0})
{
    copyString(str, buf);
    size = strlen(buf);
}

String::String(const String &str)
    : capacity(str.capacity)
    , size(str.size)
    , buf(new char[capacity]{0})
{
    std::copy(str.buf, str.buf + size + 1, buf);
}

String::String(String&& str) noexcept
    : capacity(defaultMinCapacity)
    , size(0)
    , buf(new char[capacity]{0})
{
    swap(str);
}

String::String(String const& str, std::size_t capacity)
    : capacity(capacity)        // The burden is on the user to make sure capacity is larger than size (hence it is private)
    , size(str.size)
    , buf(new char[capacity]{0})
{
    std::copy(str.buf, str.buf + size + 1, buf);
}

String& String::operator=(String str) noexcept
{
    swap(str);
    return *this;
}

String::~String()
{
    delete []buf;
}

void String::swap(String& other) noexcept
{
    using std::swap;
    swap(capacity,   other.capacity);
    swap(size,       other.size);
    swap(buf,        other.buf);
}

String& String::operator+=(String const& str)
{
    // If there is not enough space
    // To hold this and the extra string then reallocate.
    if (size + str.size >= capacity) {
        // Use private constructor that accepts a capacity.
        // To create a copy of this string with enough space.
        String  temp(*this, (size + str.size) * 2 + 1);

        // Swap the temp and this.
        swap(temp);
    }

    // Append the new string
    std::copy(str.buf, str.buf + str.size + 1, buf + size);
    size += str.size;
    return *this;
}


String String::operator+(String const& str) const
{
    String  temp(*this, (size + str.size) * 2 + 1);
    return temp += str;
}
}

int main()
{
    using ThorsAnvil::String;

    String s1;
    std::cout << s1.length() << " s1 >" << s1 << "< " << s1.maxLenght() << "\n";

    char a[] = "ab";
    String s2(a);
    std::cout << s2.length() << " s2 >" << s2 << "< " << s2.maxLenght() << "\n";

    std::string s3(a);
    std::cout << s3.length() << " s3 >" << s3 << "< " << "\n";

    String s4(s2);
    std::cout << s4.length() << " s4 >" << s4 << "< " << s4.maxLenght() << "\n";

    String s5(String("abc"));
    std::cout << s5.length() << " s5 >" << s5 << "< " << s5.maxLenght() << "\n";

    String s6=s5;
    std::cout << s6.length() << " s6 >" << s6 << "< " << s6.maxLenght() << "\n";

    String s7=String("abcd");
    std::cout << s7.length() << " s7 >" << s7 << "< " << s7.maxLenght() << "\n";

    char x[] = "String";
    std::cout << ">" << x << "<\n";
    String s8=s7+s6;
    std::cout << s8.length() << " s8 >" << s8 << "< " << s8.maxLenght() << "\n";
}
\$\endgroup\$
13
  • \$\begingroup\$ I think there is something wrong with your explanation for "Copy and Swap" for copy and move assignment. You just repeated them. @Martin York \$\endgroup\$
    – Jacob
    Commented Sep 25, 2022 at 21:16
  • \$\begingroup\$ @Jacob read it slight more carefully. There is a move and copy interchanged. They are basically identical but because you have separate move and copy constructor it will invoke the correct one when creating the parameter str thus achieving either a Copy Swap or a Move Swap. \$\endgroup\$ Commented Sep 25, 2022 at 22:47
  • \$\begingroup\$ Since you already use a std::copy function, why do you define another copyString function? @Martin York \$\endgroup\$
    – Jacob
    Commented Sep 26, 2022 at 0:05
  • \$\begingroup\$ The reason I did not use strcpy or strlen is that this is a interview question and the interviewer may not allow me to use such funtions. He always asks to realize the funtction yourself. What if the interviewer does not allow me to use std::copy and std::swap? @Martin York \$\endgroup\$
    – Jacob
    Commented Sep 26, 2022 at 0:52
  • \$\begingroup\$ Always use standard functions first. IF the interviewer asks you to write them then fine they are all pretty trivial to write. You can then write them. But by using the standard functions you are showing that you know these standard functions exists which is a plus. \$\endgroup\$ Commented Sep 26, 2022 at 1:25

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.