I was writing simple String class implementation (and it is quite ordinary), but I had found out that constructors, destructors and operator =
are the most sensitive areas.
So I wonder, if my implementation good in the sense of C++11/14 standard and are they efficient enough?
String::String()
{
m_characters = new char[0];
m_size = 0;
}
String::String( const int size )
{
m_size = size;
m_characters = new char[size];
}
String::String( const char* str )
{
m_size = 0;
int i = 0;
while ( str[i] )
{
m_size++;
i++;
}
m_characters = new char[m_size];
for (int i = 0; i < m_size; i++)
m_characters[i] = str[i];
}
String::String( const String& string )
{
m_size = string.m_size;
m_characters = new char[m_size];
for ( int i = 0; i < m_size; i++ )
m_characters[i] = string.m_characters[i];
}
String::~String()
{
delete [] m_characters;
}
String& String::operator=( const String& string )
{
if ( this != &string )
{
m_size = string.m_size;
m_characters = new char[m_size];
for (int i = 0; i < m_size; i++ )
m_characters[i] = string.m_characters[i];
}
return *this;
}
I omit the header file because I think that class interface is quite clear. If required, I can attach it.
UPD. SO does not allow me to put full code here, so I've put it on gist at github.
-
\$\begingroup\$ The implementation is non conformant: it does not include null terminator automatically. The real string will be C compatible. \$\endgroup\$Incomputable– Incomputable2017年06月08日 21:20:54 +00:00Commented Jun 8, 2017 at 21:20
-
1\$\begingroup\$ It would help if you actually posted your class, and not just the implementation of selected member-functions! \$\endgroup\$Deduplicator– Deduplicator2017年06月08日 21:34:50 +00:00Commented Jun 8, 2017 at 21:34
-
\$\begingroup\$ @Deduplicator, CW was complaining about my question being 95% of code, so I left things which were most interesting to me. I can add a link to full class on gist. \$\endgroup\$Montreal– Montreal2017年06月08日 21:42:04 +00:00Commented Jun 8, 2017 at 21:42
-
\$\begingroup\$ @Montreal, links rot. SE would like to keep the stuff for eternity. \$\endgroup\$Incomputable– Incomputable2017年06月08日 21:43:08 +00:00Commented Jun 8, 2017 at 21:43
4 Answers 4
Well, in order to evaluate your code, I'll assume (Yes, assume makes an ass out of you and me, but what can I do...) that your class is of the form:
class String {
char* m_characters;
int m_size;
public:
String();
String( const int size );
String( const char* str );
String( const String& string );
~String();
String& operator=( const String& string );
};
Now, there are various concerns with your implementation:
std::size_t
is a far superior type for sizes, as it is actually dedicated for that task instead of just sometimes coincidentally having somewhat fitting limits.You are missing any and all observer- and modifier-functions.
At least that makes the lack of a separate member for the strings capacity less keenly felt.You make no provision for the 0-terminator, meaning your strings cannot be used where c-strings are expected even after providing some observers.
You make no use of rvalue-references, and the optimizations knowing your source will be destroyed allows.
One of the main advantages of defining a separate default-constructor is the possibility of not letting it allocate any memory, meaning it can be
constexpr
andnoexcept
.Any constructor which can be called with one argument must be marked
explicit
unless using it should be used for implicit type-conversions.
The one expecting asize
is not marked.Even if you would not use
std::strlen
from the standard library, it is an utter waste to count a c-strings size twice in parallel in different variables.The standard-library contains
std::copy_n
for copying a range of known length. And it is probably better-optimized than a simple hand-rolled loop.Consider using the initializer-list where you can. Though it doesn't matter much with primitive types, it's a good habit to get into.
You should use the copy-and-swap idiom for assignment. Doing so fixes:
- The self-assignment-check is an anti-pattern because it favors the rare self-assignment over the expected case, thus being a pessimization.
- You fail to free the original memory in the assignment-operator.
As a bonus, my take on the additional members and free-functions in the full gist version:
- The indexing-operators should be
constexpr
,noexcept
, both return a reference and be defined in-class for better inlining. - You are missing
==
and!=
from your relational operators. Those are the easiest and most used ones, which makes their absence especially puzzling. Define!=
in terms of==
. - You should define all the other relational operators in terms of
<
to avoid duplicate code. And that one should usestd::lexicographical_compare
. - All relational operators should be
noexcept
andconstexpr
. - The member-function
void Print( std::ostream& os ) const
should be the free (or at least friend, you need more observers at this point) functionstd::ostream& operator<<(std::ostream& os, const String& s)
. GetSize()
should besize()
. Following interface-conventions is not only for ease of use by humans but also for templates.
Also, mark itnoexcept
and `constexpr ́.- Your
ToUpper
causes UB if called with a negativechar
. And even if that is avoided, the adjustment only works for ASCIIa-z
. You know thatislower
is locale-aware? And what's wrong with `toupper` that you are trying to correct?
There is no point in having this constructor.
String::String()
You can simulate its affects by putting a default parameter in this constructor.
class String
{
public:
explicit String(int size = 0); // Have a default parameter and
// Covers both your first two constructors.
For the next constructor:
String::String( const int size )
^^^^^ I see little point in this.
Use initializer list when you can.
This:
{
m_size = size;
m_characters = new char[size];
}
Should be:
: m_size(size)
, m_characters(new char[size])
{}
In this case it makes no difference (I will admit). But not doing it will make you classes more brittle in for future modifications. So it is always a good habit to get into.
I would argue that this constructor should test for nullptr
String::String( const char* str )
Now the standard does not. But there have been many/many arguments over this. I think you will reduce errors be detecting this and shutting them down.
Don't try and get string size yourself.
m_size = 0;
int i = 0;
while ( str[i] )
{
m_size++;
i++;
}
There is a standard library function for this strlen()
. On some systems it is even highly optimized to be quicker than stuff you can write using standard C. So use the system version it will potentially be quicker than stuff you write.
There is a standard library function for doing a copy:
for (int i = 0; i < m_size; i++)
m_characters[i] = str[i];
Prefer:
std::copy(m_characters, m_characters + m_size, str);
It will not be slower than your code but could be quicker. It also expresses intent much more clearly.
Same comments as above:
String::String( const String& string )
// Prefer initializer list.
{
m_size = string.m_size;
m_characters = new char[m_size];
// Use standard algorithms.
for ( int i = 0; i < m_size; i++ )
m_characters[i] = string.m_characters[i];
}
Destructor is very standard and will work.
String::~String()
{
delete [] m_characters;
}
The standard pattern for the assignment operator is Copy and Swap Idiom
(please look it up).
String& String::operator=( const String& string )
{
// Suspect check for self assignment.
// Does it really make things quicker?
if ( this != &string )
{
m_size = string.m_size;
// This leaks the old value.
m_characters = new char[m_size];
for (int i = 0; i < m_size; i++ )
m_characters[i] = string.m_characters[i];
}
return *this;
}
The check for self assignment. Yes it looks like it speeds the code up when you do self assignment. It does. But conversely it slows the code down when you don't have self assignment (not very much but a tiny bit). The problem is that actual self assignment is very exceptionally rare.
So the question becomes: Is the size of number of non self assignment that happen per self assignment multiplied by a tiny cost
smaller than 1 * cost of making a copy an average string
. People have tried this and found the most efficient version is not to test for self assignment but always make a copy.
String& String::operator=(String string) // Pass by value to get a copy.
{
string.swap(*this);
return *this;
}
I would also note that you missed the move operators. Move semantics can improve the performance a bit as they avoid copying in a couple of situations).
-
\$\begingroup\$ Comments are not for extended discussion; this conversation has been moved to chat. \$\endgroup\$Mathieu Guindon– Mathieu Guindon2017年06月09日 16:18:02 +00:00Commented Jun 9, 2017 at 16:18
So I wonder, if my implementation good in the sense of C++11/14 standard and are they efficient enough?
By means of c++ standards (c++11 and any earlier) you simply won't write such class at all.
You simply use std::string
and be done with it.
More points:
m_characters = new char[0];
Doesn't makes sense. Rather use
m_characters = nullptr;
String::String( const String& string )
string
is already used as a type in the c++ standard library, this may confuse your compiler in certain circumstances.
-
1\$\begingroup\$ I am aware that
std::string
exist, I am exploring some basic pointer-related C++ stuff. Concerningstring
, I do not useusing namespace std;
stuff. \$\endgroup\$Montreal– Montreal2017年06月08日 19:24:38 +00:00Commented Jun 8, 2017 at 19:24 -
\$\begingroup\$ @Montreal "I am exploring some basic pointer-related C++ stuff." You shouldn't. Rather learn about the idiomatic stuff. There's not much value in doing so when working with c++. You may want to read my blog entry about that topic. \$\endgroup\$πάντα ῥεῖ– πάντα ῥεῖ2017年06月08日 19:27:56 +00:00Commented Jun 8, 2017 at 19:27
-
1\$\begingroup\$ I disagree with not using
new char[0]
. Sometime you want to distinguish between an uninitialized string and one that is initialized. \$\endgroup\$Loki Astari– Loki Astari2017年06月08日 20:55:46 +00:00Commented Jun 8, 2017 at 20:55 -
1\$\begingroup\$ How can you tell the difference between an unallocated string and an empty string? \$\endgroup\$Loki Astari– Loki Astari2017年06月08日 21:30:40 +00:00Commented Jun 8, 2017 at 21:30
-
1\$\begingroup\$ @πάνταῥεῖ: There is nothing invalid about
new char[0]
(though it is arguably wasteful whennullptr
can signal the same high-level "state") \$\endgroup\$Lightness Races in Orbit– Lightness Races in Orbit2017年06月09日 00:58:06 +00:00Commented Jun 9, 2017 at 0:58
It can be good excercise to write such thing but in production code I would not like to see "yet-another-string-class".
That being said here is my review:
String::String()
You can omit the implementation of the default constructor if you initialize the members where they are declared. This also reduces a source of error as you can't forget to initialize members when you add a new constructor.
class String
{
std::size_t size = 0;
char* m_characters = nullptr;
public:
String() = default; // use compiler-generated default constructor
};
String::String( const int size ) { m_size = size; m_characters = new char[size]; }
It makes no sense for size
to be of a signed type. I suggest replacing int
by std::size_t
which is unsigned and will be large enough to store the biggest possible object size. On some 64-bit platforms such as Microsoft Windows int
is only 32-bit, whereas std::size_t
will be 64-bit.
for (int i = 0; i < m_size; i++) m_characters[i] = str[i];
Instead of copying the string char-by-char use memcpy()
which is pretty well optimized. It typically uses special CPU instructions that copy multiple bytes in parallel:
memcpy( m_characters, str, m_size );
-
1\$\begingroup\$ The defaulted constructor will explode. It will not make size and pointer zero, which is a bomb going to explode. Also if type is POD even VC++ will optimize it into memcpy, so calling copy is usually better. \$\endgroup\$Incomputable– Incomputable2017年06月08日 21:34:21 +00:00Commented Jun 8, 2017 at 21:34
-
4\$\begingroup\$ @Incomputable Ehm, you did see the in-class-initializers? \$\endgroup\$Deduplicator– Deduplicator2017年06月08日 21:41:43 +00:00Commented Jun 8, 2017 at 21:41
-
\$\begingroup\$ @Deduplicator, no, thank you. Though the point about copy still holds. I apologize for misunderstanding. \$\endgroup\$Incomputable– Incomputable2017年06月08日 21:44:45 +00:00Commented Jun 8, 2017 at 21:44
-
\$\begingroup\$ @zett42, in production I will just use
std::string
. \$\endgroup\$Montreal– Montreal2017年06月08日 21:47:21 +00:00Commented Jun 8, 2017 at 21:47 -
\$\begingroup\$ @Incomputable What is the advantage of
std::copy()
over directly callingmemcpy()
in this case? \$\endgroup\$zett42– zett422017年06月08日 22:11:04 +00:00Commented Jun 8, 2017 at 22:11
Explore related questions
See similar questions with these tags.