3
\$\begingroup\$

I am trying to create a string class without std::string. Just for educational purpose.

So I created some utility functions, and it would be very nice if you guys could take a look at the code for mistakes.

Does anyone see any errors? (probably)


// Includes.
#include <iostream> // for "print()" and "std".
#include <sstream> // for "tostr()".

// Print.
template <typename Arg, typename... Args>
void print(Arg&& arg, Args&&... args) {
    std::cout << std::forward<Arg>(arg);
    ((std::cout << ' ' << std::forward<Args>(args)), ...);
    std::cout << std::endl;
}

// Allocate a new array.
template <class T>
T * alloc(
    // The new size of the array.
    size_t n
) {
    T *a;
    a = (T *) malloc((n + 1) * sizeof(T));
    return a;
}

// Delete.
template <class T>
void del(T *x) {
    delete[] x;
    x = NULL;
}

// Length.
size_t len(const char* x) {
    return ::strlen(x);
}
size_t len(char x) {
    if (std::isblank(x)) { return 0; }
    return 1;
}

// Cast const char* to char *.
char* strcast(const char* x, size_t n = -1) {
    char *s;
    if (n == -1) { n = len(x); }
    s = alloc<char>(n); // real alloc size afterwards is n + 1
    if (n == 0) { 
        s[0] = '\0';
        return s; 
    }
    return strcat(strcpy(s, x), "");
}

// String is null.
// Will cause segmentation fault if x is unallocated [const char x;], when x is [const char *x = ""] it will run.
bool strnull(const char* x) {
    return (x == NULL || ! *x);
}

// Safely dump a string.
// Not necessary.
// Will cause segmentation fault if x is [const char x;], when x is [const char *x = ""] it will run.
// const char* strdump(const char* x) {
//     return x;
// }
// char* strdump(char* x) {
//     return x;
// }

// String equals.
// Will again cause segfault if x / y is unallocated [const char x;].
bool streq(const char* x, const char* y) {
    return strcmp(x, y) == 0;
}

// Char equals.
// Does not cause segfault if a / b is unallocated [char x].
bool chareq(char a, char b) {
    if (std::isspace(a) and std::isspace(b)) { return true; }
    return not (a < b) and not (a > b) and a == b;
}

// String from double.
template <class T>
char* tostr(T x) {
    std::stringstream ss;
    ss << x;
    char *y;
    y = ss.str().data();
    return y;
}

// From string to double.
// Will cause segmentation fault if x is unallocated [const char x;], when x is [const char *x = ""] it will run.
float fromstr(const char* x) {
    if (strnull(x)) { return 0; }
    return std::atof(x);
}

// Concatenate multiple char*.
// Will cause segmentation fault if x / y is unallocated [const char x;], when x / y is [const char *x = ""] it will run.
char *strconcat(char *x, const char*y) {
    size_t xn = strlen(x);
    size_t yn = strlen(y);
    char *z = alloc<char>(xn + yn);
    memcpy(z, x, xn);
    memcpy(z + xn, y, yn);
    return z;
}
const char *strconcat(const char *x, const char *y) {
    size_t xn = strlen(x);
    size_t yn = strlen(y);
    char *z = alloc<char>(xn + yn);
    memcpy(z, x, xn);
    memcpy(z + xn, y, yn);
    return z;
}
char *strconcat(char *x) { return x; }
const char *strconcat(const char *x) { return x; }
template <class... Args> char *strconcat(char *x, const char *y, Args&&... args) {  return strconcat(strconcat(x, y), args...); }
template <class... Args> const char *strconcat(const char *x, const char *y, Args&&... args) { return strconcat(strconcat(x, y), args...); }

// Find substring in str.
// Will cause segmentation fault if str / substr is unallocated [const char x;], when str / substr is [const char *x = ""] it will run.
// class npos {
// public:
//  size_t m_pos;
//  bool found = false;
//  npos() {};
//  npos(size_t pos) { m_pos = pos; found = true; }
//  npos(int pos) {  if (pos > 0) { m_pos = pos; found = true; } }
//  operator size_t() { return m_pos; }
// };
class nposclass {
public:
    size_t m_pos;
    nposclass() {}
    operator size_t() { return m_pos; }
};
nposclass npos = nposclass();
size_t strfind(char* str, const char* substr) {
    char *dest = strstr(str, substr);
    if (dest == NULL) { return npos; };
    return dest - str;
}
size_t strfind(char* str, const char* substr, size_t start) {
    size_t i = strfind(str + start, substr);
    if (i != -1) { return start + i; }
    return i;
}

// Replace a substring in a string.
// Will cause segmentation fault if str / from / to is unallocated [const char x;], when str / from / to is [const char *x = ""] it will run.
char* strreplace(char* str, const char* from, const char* to, double increaser = 2.0) {
    if (strnull(from)) { return str; }
    size_t nt = strlen(to); // to size.
    size_t nf = strlen(from); // from size.
    size_t n = strlen(str); // str size.
    size_t mn = n; // max alloc size.
    char *result = alloc<char>(mn);
    size_t nn = 0; // new size.
    while (*str) {

        // Found from.
        if (strstr(str, from) == str) {

            // Check resize.
            if (nn + nt > mn) {
                mn = (nn + nt) * increaser;
                char *a = alloc<char>(mn);
                memcpy(a, result, nn);
                delete[] result; result = NULL;
                result = a;
            }

            // Add to.
            memcpy(&result[nn], to, nt);
            nn += nt;
            str += nf;

        }

        // Add current char.
        else { result[nn++] = *str++; }
    }
    result[nn] = '\0';
    return result;
}


// Testing.
// Test code produces the warnings "ISO C++11 does not allow conversion from string literal to 'char *'", but that will be fixed by creating a string object.
int main() {

    // strcast.
    const char* x = "Hello";
    char *y = strcast(x);
    print("strcast:",y);

    // strnull.
    const char* x0 = "s ";
    const char* x1 = "";
    print("len: 2 ==>",strnull(x0));    
    print("len: 0 ==>", strnull(x1));    

    // len.
    print("strnull: 0 ==>",strnull(x0));    
    print("strnull: 1 ==>", strnull(x1));       

    // streq.
    print("streq: 0 ==>",streq(x0, x1));    
    print("streq: 1 ==>", streq(x1, x1));    

    // chareq.
    char c;
    print("chareq: 0 ==>", chareq('a', 'b'));    
    print("chareq: 1 ==>",chareq('a', 'a'));    
    print("chareq: 0 ==>",chareq('a', c));    

    // tostr.
    char * y0 = tostr<double>(1.1);
    print("tostr: 1.1 ==>", y0);    

    // fromstr.
    double d0 = fromstr("1.1");
    print("fromstr: 1.1 ==>", d0);   

    // Strconcat.
    const char *x2 = "Hello";
    x2 = strconcat(x2, " World!", " Howdy!");
    print("strconcat: Hello World! Howdy =", x2);
    char *y1 = "Hello";
    y1 = strconcat(y1, " World!", " Howdy!");
    print("strconcat: Hello World! Howdy =", y1); 

    // strfind.
    print("strfind: 4 ==>", strfind("Hello World!", "o"));
    print("strfind: 7 ==>", strfind("Hello World!", "o", 5));
    print("strfind: 1 ==>", strfind("Hello World!", "?") == npos);
    print("strfind: 0 ==>", strfind("Hello World!", "?") == 10);

    // strreplace.
    print("strreplace: Hello Universe! ==>", strreplace("Hello World!", "World", "Universe"));
    print("strreplace: Hello! ==>", strreplace("Hello World!", " World", ""));
    print("strreplace: Hello World! ==>", strreplace("Hello World!", "", ""));

    return 0;
}
```
\$\endgroup\$
1
  • 3
    \$\begingroup\$ Don't replace reviewed code. Either post a follow-up, or an answer with list and rationale of what you changed. \$\endgroup\$ Jul 9, 2022 at 16:59

1 Answer 1

4
\$\begingroup\$

Renaming built-in functions

If you are writing these functions for your own education, then I don't think you should be using built-in functions to implement them.

size_t len(const char* x) {
    return ::strlen(x);
}

This could be written without any library functions as a for-loop using your len(char) function (if that's how you want to define the length of your string).

Similarly, these functions also merely rename language built-ins:

  • alloc just calls malloc() with nearly the same arguments (it should call new, see below).
  • del just calls delete[]
  • strfind just calls strstr()
  • fromstr just calls std::atof

You should be trying to write your functions without using the language's built-in functions so you can discover how they work. This does not apply to such low-level functions as malloc and delete[], but all functions operating on strings are doable.

Write your more complex functions in terms of the simpler functions you've written. You only use your len() function once and call strlen() everywhere else.


// String from double.
template <class T>
char* tostr(T x) {
    std::stringstream ss;
    ss << x;
    char *y;
    y = ss.str().data();
    return y;
}

Since ss.str() creates a std::string, this seems to go against the goals of this code.

Another problem with tostr() above: the char array returned by .data() is not null-terminated, so it can't be used with most string functions. You should use .c_str().

And another problem: you are returning the data from a temporary string that will be destroyed at the end of the function. When I run your code, I get the line: tostr: 1.1 ==> ⁿ⌡». I ran it again and got this: tostr: 1.1 ==> 4≈§☺<►. The function returns a pointer to destroyed data. You need to allocate space for the resulting string, store the pointer to that data in y, copy the new data to that pointer, and then return that pointer to the explicitly allocated data (that is, y).

Undefined behavior

Your alloc and del cannot be used together. Memory allocated with malloc must be deleted by calling free. The operator delete should only be used with pointers allocated with new. Mixing up these operators leads to undefined behavior, which means the compiler makes no guarantees about what the resulting program will do. Since this is C++, use new and delete.


class nposclass {
public:
    size_t m_pos;
    nposclass() {}
    operator size_t() { return m_pos; }
};

The member m_pos is never assigned a value. Using the class like this is undefined behavior and may or may not function correctly.

Similary, char c in main() is never assigned a value.

Non-compiling code

If you want a function to be able to take a string literal as an argument, then it must take a const char*, not a char*. String literals cannot be modified. This is also why you cannot call strreplace on a string literal in the final lines of your main() function.

Functions like strfind should take const char* because a function that searches a string should not modify the string.

Other bits

    // strnull.
    const char* x0 = "s ";
    const char* x1 = "";
    print("len: 2 ==>",strnull(x0));    
    print("len: 0 ==>", strnull(x1));

You probably meant to test len() here, right?

The function strcast() is not really a cast. It creates a copy of a string. It should be renamed to reflect this behavior.

In your chareq() function, what does not (a < b) and not (a > b) and a == b accomplish that a == b doesn't?

Also, even though and, not, and or are synonyms for &&, !, and ||, respectively, they are very rarely used by C++ programmers. Using these words will cause some programmers to wonder if they opened a Python source file by mistake. :)

In many of your functions, spaces are treated specially, either by not being counted in the length of a char as in len(char) or by any space character being equal to any other space character--as in chareq(char, char). This should be documented somewhere in the code as a comment.

\$\endgroup\$
14
  • \$\begingroup\$ Thank you very much for such a detailed & understandable answer! Extremely useful. I just read it and I had already made a lot of changes, some were already fixed. And a lot not lol. What exactly do you mean with italic "length of a char as in len(char) or by any space character being equal to any other space character" italic . I understand what you mean in 'chareq' but not in 'len'. Or do you mean what happens in 'strlen'? \$\endgroup\$
    – user260583
    Jul 9, 2022 at 16:58
  • \$\begingroup\$ And I would love to create this without using default functions, though recreating functions as 'strstr' with equal performance is an issue. \$\endgroup\$
    – user260583
    Jul 9, 2022 at 16:59
  • 1
    \$\begingroup\$ @user260583 Answering your questions in order: My comment on the string length functions is that, since your len(char) function defines a blank character has having zero length, I thought your other string length functions would also ignore blank characters. If this is true, then your functions should act consistently. Otherwise, len(char) acts differently than len(char*) for an unknown reason. Consistency helps readers of your code understand what your program is doing. \$\endgroup\$
    – Mark H
    Jul 10, 2022 at 4:01
  • 1
    \$\begingroup\$ @user260583 It is very likely that the functions you write will not perform as well. The C++ library functions have been under development for decades (even longer than C++ has existed, since many are imported from C). However, figuring out how to write these functions yourself will be good practice. Substring search has a lot of research behind it if you want to learn more advanced algorithms, some of which can be found here: en.wikipedia.org/wiki/String-searching_algorithm \$\endgroup\$
    – Mark H
    Jul 10, 2022 at 4:05
  • 1
    \$\begingroup\$ Is there really a need to achieve the same performance? For educational purpose, it's just nice to get it working. You could then compare your implementation to the stdlib one. \$\endgroup\$ Jul 10, 2022 at 7:10

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.