Bug: Initial allocation not cleared to zero
Your initial allocation of sb->mem uses malloc instead of calloc, so its contents are uninitialized. If you then append a few characters and call sb_as_string(), you will get back a string that is not properly terminated. You should use calloc instead.
Minor bug
If your call to realloc fails, your buffer will be incorrect because it will no longer be null terminated (you just appended a character to the last spot). You should either rewrite a '\0' to the end of the buffer if realloc fails, or do the realloc before you append the character.
Argument check
When creating a string buffer, you should handle the case where init_cap is passed in as 0. You can set it to some default value in that case. Right now, an initial capacity of 0 will cause a crash down the line because your append function will append to a zero length buffer without ever reallocating.
Usability
I would much prefer an append function that took a string argument instead of a character argument. I'm not sure that I would ever need to append one character at a time.
Also, it might be nice to have a to_string type function that returns the string but also frees the stringbuilder. The way you currently have it, you can retrieve the string, but if you subsequently free the stringbuilder it will also free the string you just retrieved. That makes it difficult to use the string because its lifetime is tied to the lifetime of the stringbuilder.
I've written a bunch of code that prints values of various different types which works great but rather than printing to stdout I'd like to capture the result in a big string. In other languages this would be achieved by appending to a string builder or appending strings to an array and then concatenating them all. Is there an idiomatic way to do this in vanilla C? If not, what library would you recommend (M1/M2 Mac OS and Raspberry Pi)?
Videos
Bug: Initial allocation not cleared to zero
Your initial allocation of sb->mem uses malloc instead of calloc, so its contents are uninitialized. If you then append a few characters and call sb_as_string(), you will get back a string that is not properly terminated. You should use calloc instead.
Minor bug
If your call to realloc fails, your buffer will be incorrect because it will no longer be null terminated (you just appended a character to the last spot). You should either rewrite a '\0' to the end of the buffer if realloc fails, or do the realloc before you append the character.
Argument check
When creating a string buffer, you should handle the case where init_cap is passed in as 0. You can set it to some default value in that case. Right now, an initial capacity of 0 will cause a crash down the line because your append function will append to a zero length buffer without ever reallocating.
Usability
I would much prefer an append function that took a string argument instead of a character argument. I'm not sure that I would ever need to append one character at a time.
Also, it might be nice to have a to_string type function that returns the string but also frees the stringbuilder. The way you currently have it, you can retrieve the string, but if you subsequently free the stringbuilder it will also free the string you just retrieved. That makes it difficult to use the string because its lifetime is tied to the lifetime of the stringbuilder.
To be more comprehensive, the line in sb_append():
memset(new_mem + to->cap, 0, to->cap);
should become:
memset(new_mem + to->cap, 0, (to->cap) * (LOAD_FACTOR - 1));
That matters when LOAD_FACTOR is set to something greater than 2.
thanks for sharing this code.
The C++ way would be to use std::stringstream or just plain string concatenations. C++ strings are mutable so the performance considerations of concatenation are less of a concern.
with regards to formatting, you can do all the same formatting on a stream, but in a different way, similar to cout. or you can use a strongly typed functor which encapsulates this and provides a String.Format like interface e.g. boost::format
The std::string.append function isn't a good option because it doesn't accept many forms of data. A more useful alternative is to use std::stringstream; like so:
#include <sstream>
// ...
std::stringstream ss;
//put arbitrary formatted data into the stream
ss << 4.5 << ", " << 4 << " whatever";
//convert the stream buffer into a string
std::string str = ss.str();
A string instance is immutable. You cannot change it after it was created. Any operation that appears to change the string instead returns a new instance:
string foo = "Foo";
// returns a new string instance instead of changing the old one
string bar = foo.Replace('o', 'a');
string baz = foo + "bar"; // ditto here
Immutable objects have some nice properties, such as they can be used across threads without fearing synchronization problems or that you can simply hand out your private backing fields directly without fearing that someone changes objects they shouldn't be changing (see arrays or mutable lists, which often need to be copied before returning them if that's not desired). But when used carelessly they may create severe performance problems (as nearly anything – if you need an example from a language that prides itself on speed of execution then look at C's string manipulation functions).
When you need a mutable string, such as one you're contructing piece-wise or where you change lots of things, then you'll need a StringBuilder which is a buffer of characters that can be changed. This has, for the most part, performance implications. If you want a mutable string and instead do it with a normal string instance, then you'll end up with creating and destroying lots of objects unnecessarily, whereas a StringBuilder instance itself will change, negating the need for many new objects.
Simple example: The following will make many programmers cringe with pain:
string s = string.Empty;
for (i = 0; i < 1000; i++) {
s += i.ToString() + " ";
}
You'll end up creating 2001 strings here, 2000 of which are thrown away. The same example using StringBuilder:
StringBuilder sb = new StringBuilder();
for (i = 0; i < 1000; i++) {
sb.Append(i);
sb.Append(' ');
}
This should place much less stress on the memory allocator :-)
It should be noted however, that the C# compiler is reasonably smart when it comes to strings. For example, the following line
string foo = "abc" + "def" + "efg" + "hij";
will be joined by the compiler, leaving only a single string at runtime. Similarly, lines such as
string foo = a + b + c + d + e + f;
will be rewritten to
string foo = string.Concat(a, b, c, d, e, f);
so you don't have to pay for five nonsensical concatenations which would be the naïve way of handling that. This won't save you in loops as above (unless the compiler unrolls the loop but I think only the JIT may actually do so and better don't bet on that).
String vs. StringBuilder
String
Under System namespace
Immutable (readonly) instance
Performance degrades when continuous change of value occurs
Thread-safe
StringBuilder (mutable string)
- Under System.Text namespace
- Mutable instance
- Shows better performance since new changes are made to an existing instance
For a descriptive article about this topic with a lot of examples using ObjectIDGenerator, follow this link.
Related Stack Overflow question: Mutability of string when string doesn't change in C#