Thread: Help with Strings

  1. #1
    Registered User
    Join Date
    Aug 2019
    Location
    inside a singularity
    Posts
    308

    Help with Strings

    Hey, just wanted to know which one of the following codes will be more optimized for return values. I'm just playing around with writing my own String class and experimenting for optimisation.


    Code:
    String func (void)
    {
        return "A String"; 
    }
    
    
    /* Constructor to initialise a const char* called for typecasting "A String" to my String data type and then
       Copy Constructor invoked for returning "A String" */

    or


    Code:
    String& func (void)
    {
        String* string = new String("Another String");
        
        return string;
    }
    
    
    /* If I do it this way, only the Constructor to initialise const char* is called and the function returns a
       reference to that block of memory on the heap but then I run into the problem of de-allocating the
       memory. However, for the sake of argument let's say that the memory does get de-allocated at the end of
       the program. Then, which of the two functions will be a more optimised way of returning the string? */

    Also, I just want to share a mistake I learnt from while playing with my String class.
    Code:
    String& func (void)
    {
        String s = "Don't do this";
    
    
        return s;
    
    
        // RIP
    }

    Thanks, any help will be appreciated

  2. #2
    C++ Witch laserlight's Avatar
    Join Date
    Oct 2003
    Location
    Singapore
    Posts
    28,413
    If all you want to do is avoid a possible copy constructor call, then indeed the second example is better (if you return *string instead), but it is also so error-prone that you should live with the copy constructor call...

    ... but you don't have to. Even pre-C++11, copy elision would happen for the first example if you're initialising a String with the return value. Post-C++11, we have move semantics, so if you properly define the move constructor and move assignment operator (or use member objects that have move semantics), you can more generally return by value and a move instead of copy would happen where appropriate (or forced through the use of std::move causing the functions with move semantics to be selected).

    Alex once wrote an article on this, though you could always search the Web if you don't understand his explanation.
    Last edited by laserlight; 10-17-2019 at 04:35 AM.
    Quote Originally Posted by Bjarne Stroustrup (2000-10-14)
    I get maybe two dozen requests for help with some sort of programming or design problem every day. Most have more sense than to send me hundreds of lines of code. If they do, I ask them to find the smallest example that exhibits the problem and send me that. Mostly, they then find the error themselves. "Finding the smallest program that demonstrates the error" is a powerful debugging tool.
    Look up a C++ Reference and learn How To Ask Questions The Smart Way

  3. #3
    Registered User
    Join Date
    Aug 2019
    Location
    inside a singularity
    Posts
    308
    Hey!

    I've been playing around with my String class (that I've been wanting to code, for a while now) and my recent interest in optimisation has been a driving force to test the randomest of things. I highly doubt if I'll ever be using any kind of example as shared above in actual code but it'll be better to learn what happens now than later.

    > then indeed the second example is better (if you return *string instead)

    Why would it be *string? func () returns a String&, and string is declared as a String*. So, shouldn't it just be "return string"?

    > Even pre-C++11, copy elision would happen for the first example if you're initialising a String with the return value.

    Okay, I didn't know about copy elision till right now (just read more about the topic on GeeksforGeeks and Wiki). I messed around with my compiler optimisation settings and I did get two different results.
    - (All optimisations disabled)
    Default Constructor called
    Copy Constructor called

    - (Optimisation on)
    Default Constructor called

    I don't know anything about std::move so I'll let that stay for now until I learn more about it.

    --------------

    Also, how would you suggest deleting allocated memory in Example 2 once the return is done?
    Thanks for referring me to the article!
    Last edited by Zeus_; 10-17-2019 at 06:38 AM. Reason: Removed extra spacing added from typing on Notepad

  4. #4
    Registered User
    Join Date
    Aug 2019
    Location
    inside a singularity
    Posts
    308
    Code:
    #include <iostream>
    
    
    class String
    {
    private: /* Variables */
    
    
        char*  m_Text;
        size_t m_Size;
    
    
    private: /* Method Declaration and/or Definitions */
    
    
        size_t StringLength (const char* _String) const
        {
            size_t Position = 0;
    
    
            while (_String[Position] != '\0') Position++;
    
    
            return Position;
        }
    
    
        void StringCopy (const char* _String)
        {
            size_t Position = 0;
    
    
            while (Position < m_Size)
            {
                m_Text[Position] = _String[Position];
                Position++;
            }
        }
    
    
    public: /* Constructors and Destructor */
    
    
        String ()
            : m_Text(nullptr) ,
              m_Size(0)
        { }
    
    
        String (const char* _String)
        {
            m_Size = StringLength(_String);
            m_Text = new char [m_Size + 1]; // to accomodate for a null terminator
    
    
            StringCopy(_String);
        }
    
    
        String (const String& _String)
        {
            m_Size = _String.m_Size;
    
    
            m_Text = new char [m_Size];
    
    
            StringCopy(_String.m_Text);
        }
    
    
        ~String ()
        {
            delete[] m_Text;
        }
    
    
    public: /* Getters and Setters */
    
    
        size_t Size (void);
    
    
    public: /* Method Declaration and/or Definitions */
    
    
        friend std::ostream& operator << (const std::ostream& stream , const String& _String);
    
    
    };
    
    
    inline size_t String::Size (void)
    {
        return m_Size;
    }
    
    
    std::ostream& operator << (std::ostream& stream , const String& _String)
    {
        return stream << _String.m_Text;
    }
    
    
    int main (void)
    {
        String a = "Hello";
        String b = "World";
    
    
        std::cout << a << b;
    
    
        return 0;
    }
    Why is this code flagging me an error saying m_Text is a private variable? I've done operator overloading after quite sometime so might be rusty but I'm positive that the above code should work because I've declared ostream& operator << as a friend function. Am I missing something here?

  5. #5
    Registered User
    Join Date
    Aug 2019
    Location
    inside a singularity
    Posts
    308
    Also, as I've enabled all warnings, I'm getting ones that I've never seen before...
    Why am I getting these? (Specifically the ones saying I need to initialise them using member initialisation lists

    ||=== Build: Debug in Z_String (compiler: GNU GCC Compiler) ===|C:\Programming\CodeBlocks\share\Zeus\Z_String\ main.cpp|3|warning: 'class String' has pointer data members [-Weffc++]|
    C:\Programming\CodeBlocks\share\Zeus\Z_String\main .cpp|3|warning: but does not override 'operator=(const String&)' [-Weffc++]|
    C:\Programming\CodeBlocks\share\Zeus\Z_String\main .cpp||In constructor 'String::String(const char*)':|
    C:\Programming\CodeBlocks\share\Zeus\Z_String\main .cpp|39|warning: 'String::m_Text' should be initialized in the member initialization list [-Weffc++]|
    C:\Programming\CodeBlocks\share\Zeus\Z_String\main .cpp|39|warning: 'String::m_Size' should be initialized in the member initialization list [-Weffc++]|
    C:\Programming\CodeBlocks\share\Zeus\Z_String\main .cpp|47|warning: 'String::m_Text' should be initialized in the member initialization list [-Weffc++]|
    C:\Programming\CodeBlocks\share\Zeus\Z_String\main .cpp|47|warning: 'String::m_Size' should be initialized in the member initialization list [-Weffc++]|
    C:\Programming\CodeBlocks\share\Zeus\Z_String\main .cpp||In function 'std::ostream& operator<<(std::ostream&, const String&)':|
    C:\Programming\CodeBlocks\share\Zeus\Z_String\main .cpp|7|error: 'char* String::m_Text' is private|
    C:\Programming\CodeBlocks\share\Zeus\Z_String\main .cpp|78|error: within this context|
    ||=== Build failed: 2 error(s), 6 warning(s) (0 minute(s), 0 second(s)) ===|


    I'll overload the = operator in sometime after I fix the << overload

  6. #6
    Registered User
    Join Date
    May 2010
    Posts
    4,633
    Your "friend" definition doesn't match the implementation, you have an extra "const" in the definition. My compiler actually had one other message that helped find the issue:

    main.cpp|92|warning: no previous declaration for ‘std:stream& operator<<(std:stream&, const String&)’ [-Wmissing-declarations]|
    main.cpp||In function ‘std:stream& operator<<(std:stream&, const String&)’:|
    main.cpp|94|error: ‘char* String::m_Text’ is private within this context|
    main.cpp|9|note: declared private here|
    Also be careful, variable names starting with an underscore followed by an upper case letter are reserved for the implementation, in any content. IMO, using any leading underscore is a mistake waiting to happen, leave the leading underscore for the compiler.


    'String::m_Text' should be initialized in the member initialization list [-Weffc++]|
    Do you know that this is telling you to use constructor initialization lists (like you did with the no argument constructor).?

  7. #7
    Registered User
    Join Date
    May 2010
    Posts
    4,633
    Your "friend" definition doesn't match the implementation, you have an extra "const" in the definition. My compiler actually had one other message that helped find the issue:

    main.cpp|92|warning: no previous declaration for ‘std::ostream& operator<<(std::ostream&, const String&)’ [-Wmissing-declarations]|
    main.cpp||In function ‘std::ostream& operator<<(std::ostream&, const String&)’:|
    main.cpp|94|error: ‘char* String::m_Text’ is private within this context|
    main.cpp|9|note: declared private here|
    Also be careful, variable names starting with an underscore followed by an upper case letter are reserved for the implementation, in any content. IMO, using any leading underscore is a mistake waiting to happen, leave the leading underscore for the compiler.


    'String::m_Text' should be initialized in the member initialization list [-Weffc++]|
    Do you know that this is telling you to use constructor initialization lists (like you did with the no argument constructor).?

  8. #8
    Registered User
    Join Date
    Aug 2019
    Location
    inside a singularity
    Posts
    308
    > Your "friend" definition doesn't match the implementation, you have an extra "const" in the definition. My compiler actually had one other message that helped find the issue:

    Thanks for spotting that out. I have no idea how I overlooked it and why did I even put a const there...

    >
    Also be careful, variable names starting with an underscore followed by an upper case letter are reserved for the implementation, in any content. IMO, using any leading underscore is a mistake waiting to happen, leave the leading underscore for the compiler.

    Will keep this in mind and change my variable names

    >
    Do you know that this is telling you to use constructor initialization lists (like you did with the no argument constructor).?

    Yes, I understand that. But why is it like a warn-able? Why is it necessary(well, not exactly necessary as it's just a warning) to only do it through a member initializer list?
    I think it's something to deal with junk values. Not sure tho.
    I've fixed all warnings now by adding the the member initialization lists in the same way I've done it for the first. It makes more sense now - all those warnings.

    Thanks for your time @jimblumberg!

  9. #9
    Registered User
    Join Date
    May 2010
    Posts
    4,633
    But why is it like a warn-able?
    Because it is inefficient to "initialize" your class variables in the body of the constructor.

    Why is it necessary(well, not exactly necessary as it's just a warning)
    At this stage of your journey you should treat all warnings as errors and fix them. Once you have gained mastery of the language then maybe you can decide what warnings are not important.

    to only do it through a member initializer list?
    I think it's something to deal with junk values. Not sure tho.
    Not anything to do with "junk values" it has to do with the possibility of calling default constructors in the "initialization" and then calling either the assignment operator or the copy constructor within the body of the constructor.

    Also did you mean for your String to be just an array of char? If you properly terminate that array you could use the C str??? functions instead of all the loops.

    Lastly, for now, I recommend that you get rid of all those redundant private: and public: statements, IMO they just obscure the code just use one of each. And do you know that "friends" are neither public, private, or protected?

  10. #10
    Registered User
    Join Date
    Aug 2019
    Location
    inside a singularity
    Posts
    308
    > At this stage of your journey you should treat all warnings as errors and fix them. Once you have gained mastery of the language then maybe you can decide what warnings are not important.

    Will do. I generally don't like warnings as compared to errors tbh and so I tend to fix them if I know how to.

    >
    Also did you mean for your String to be just an array of char? If you properly terminate that array you could use the C str??? functions instead of all the loops.

    I don't know how C functions like strcpy or strcat work in the background. So, just for practice and the way I think they work in the background, I've implemented my own functions.
    As for my String class being just an array of chars, yes, it's just a wrapper around that which will have most functions similar to std::string. However, all similar functions that I write such as find(), at(), etc will be my interpretation of what's going on in the background. Once I finish writing my own code for what I think is std::string, I will look into the actual header file for its implementation and compare why my code is worse/better(will definitely not be better in any area...) compared to the std::string class. It will be a great experience to learn from.

    >
    Lastly, for now, I recommend that you get rid of all those redundant private: and public: statements, IMO they just obscure the code just use one of each.

    I actually like to write code that way and group together common things and label them apart. Soooo, maybe for now I'll just let it stay that way and eventually down the line adopt a more professional way of writing code.

    >
    And do you know that "friends" are neither public, private, or protected?

    No, I actually didn't know that. I thought of friends just being a way of accessing private data members by non-member functions.
    In my declaration of "ostream& operator <<" above as a friend, does it count as a member function or a non-member function?

  11. #11
    Registered User
    Join Date
    Aug 2019
    Location
    inside a singularity
    Posts
    308

    Inlining

    Code:
    class String
    {
    
    private: /* Variables */
    
        char*  m_Text;
        size_t m_Size;
    
    private: /* Method Declarations and/or Definitions */
    
        size_t StringLength (const char* _String) const
        {
            size_t Position = 0;
    
            while (_String[Position] != '\0') Position++;
    
            return Position;
        }
    
        void StringCopy (const char* _String)
        {
            size_t Position = 0;
    
            while (Position < m_Size)
            {
                m_Text[Position] = _String[Position];
                Position++;
            }
        }
    
    public: /* Constructors and Destructor */
    
        String ();
    
        String (const char* _String);
    
        String (const String& _String);
    
        ~String ();
    
    public: /* Getters and Setters */
    
        inline const size_t& Size (void) const;
    
    public: /* Method Declarations and/or Definitions */
    
        friend std::ostream& operator << (std::ostream& stream , const String& _String);
    
    };
    
    /* Constructors and Destructor */
    
    String::String ()
        : m_Text(nullptr) ,
          m_Size(0)
    { }
    
    String::String (const char* _String)
        : m_Text(nullptr) ,
          m_Size(0)
    {
        m_Size = StringLength(_String);
        m_Text = new char [m_Size + 1]; // to accomodate for a null terminator
    
        StringCopy(_String);
    }
    
    String::String (const String& _String)
        : m_Text(nullptr) ,
          m_Size(0)
    {
        m_Size = _String.m_Size;
    
        m_Text = new char [m_Size];
    
        StringCopy(_String.m_Text);
    }
    
    String::~String ()
    {
        delete[] m_Text;
    }
    
    /* Getters and Setters */
    
    inline const size_t& String::Size (void) const
    {
        return m_Size;
    }
    
    /* Method Declarations and/or Definitions */
    
    std::ostream& operator << (std::ostream& stream , const String& _String)
    {
        return stream << _String.m_Text;
    }
    When and why exactly should I be inlining code? Does the compiler automatically inline code of a method defined outside the class? If I like being explicit about everything, which I do, should I be using "inline" in the Constructor Statements too (like so, "inline String::String ()") or does that mess up code? What exactly qualifies as an inline method/function?
    Please suggest me other features that I should or atleast try adding other than my String class just being a wrapper for a C-style string. I'm thinking of adding things like reference counting and some other features I've come across using but I'll do so only when I have a basic working structure ready...

    Thanks for your time and help!

  12. #12
    Registered User
    Join Date
    May 2010
    Posts
    4,633
    In my declaration of "ostream& operator <<" above as a friend, does it count as a member function or a non-member function?
    It's a friend function, not a true member function, more like an enhanced non-member function.

    When and why exactly should I be inlining code?
    Inline code can speed execution at the cost of larger executable so it is a trade off. And realize that even if the code is "inlined" in the function definition doesn't necessarily mean it will be "inlined" by the compiler.

    Does the compiler automatically inline code of a method defined outside the class?
    No. Normally code outside the class definition is not inline by default.

    If I like being explicit about everything, which I do, should I be using "inline" in the Constructor Statements too (like so, "inline String::String ()") or does that mess up code?
    No. it just obscures the code.

    What exactly qualifies as an inline method/function?
    At this point I don't think you should be worrying about inline code, just get your code to compile correctly. Once you start getting into much larger code bases you may want to play with inline. And remember that just because you add the inline keyword to some function doesn't mean the compiler will inline that code. The inline code is just a hint to the compiler and some code may never be inline.

    Please suggest me other features that I should or atleast try adding other than my String class just being a wrapper for a C-style string.
    Well right now your String class is not just a wrapper for a C-style string because you're just using C style char arrays. A C-string is an array of char terminated by the end of string character '\0', all C-string methods rely on this termination character for proper functioning. As far as adding other functionality I recommend you look at some documentation for std::string and try to implement most, if not all of that functionality.

    I'm thinking of adding things like reference counting and some other features I've come across using but I'll do so only when I have a basic working structure ready...
    What do you think you will gain by using "reference counting"?

  13. #13
    C++ Witch laserlight's Avatar
    Join Date
    Oct 2003
    Location
    Singapore
    Posts
    28,413
    Quote Originally Posted by Zeus_
    Why would it be *string? func () returns a String&, and string is declared as a String*. So, shouldn't it just be "return string"?
    You cannot implicitly convert a pointer to a String to a reference to a String; that's why you need to dereference the pointer to get a String object for the String reference to refer to.

    Quote Originally Posted by Zeus_
    Also, how would you suggest deleting allocated memory in Example 2 once the return is done?
    Just don't do example 2. It's Bad because people won't realise that manual memory management needs to be done. If you must, return a pointer, perhaps a std::unique_ptr instead.

    Returning a reference hints that the memory doesn't need to be manually managed, but in this case it must be! Returning a pointer makes the user of your function check if they need to manage the memory. Returning a unique_ptr explicitly says "here, ownership of the memory is with this smart pointer, so handle accordingly".

    Quote Originally Posted by Zeus_
    C:\Programming\CodeBlocks\share\Zeus\Z_String\ main.cpp|3|warning: 'class String' has pointer data members [-Weffc++]|
    C:\Programming\CodeBlocks\share\Zeus\Z_String\main .cpp|3|warning: but does not override 'operator=(const String&)' [-Weffc++]|
    This is an important warning. You are writing a class that automates memory management by itself rather than by storing member objects that handle their own memory management. Therefore, you must implement the destructor, and either implement both the copy constructor and copy assignment operator, or disable them. (And of course for efficiency you could implement both the move constructor and move assignment operator, which can be done even if you disable copying).

    If you don't do this, you could end up with double deletion after copy assignment.
    Last edited by laserlight; 10-17-2019 at 04:28 PM.
    Quote Originally Posted by Bjarne Stroustrup (2000-10-14)
    I get maybe two dozen requests for help with some sort of programming or design problem every day. Most have more sense than to send me hundreds of lines of code. If they do, I ask them to find the smallest example that exhibits the problem and send me that. Mostly, they then find the error themselves. "Finding the smallest program that demonstrates the error" is a powerful debugging tool.
    Look up a C++ Reference and learn How To Ask Questions The Smart Way

  14. #14
    Registered User
    Join Date
    Aug 2019
    Location
    inside a singularity
    Posts
    308
    > At this point I don't think you should be worrying about inline code, just get your code to compile correctly. Once you start getting into much larger code bases you may want to play with inline. And remember that just because you add the inline keyword to some function doesn't mean the compiler will inline that code. The inline code is just a hint to the compiler and some code may never be inline.

    Okay, will keep that in mind.

    > What do you think you will gain by using "reference counting"?

    I'm not exactly sure but it'd be a good practice at something I haven't done before. What I would implement is this: All copy assignments will point to the same block of memory until either of the copies are required to be modified. In such case, a new copy will be made elsewhere in memory. Once reference count hits zero, I'd deallocate the memory that was being held by the copies.

    Thanks for the help @jimblumberg!

  15. #15
    Registered User
    Join Date
    Aug 2019
    Location
    inside a singularity
    Posts
    308
    > You cannot implicitly convert a pointer to a String to a reference to a String; that's why you need to dereference the pointer to get a String object for the String reference to refer to.

    Oh, I need to do my revision in Pointers and References I guess.

    >
    This is an important warning. You are writing a class that automates memory management by itself rather than by storing member objects that handle their own memory management. Therefore, you must implement the destructor, and either implement both the copy constructor and copy assignment operator, or disable them. (And of course for efficiency you could implement both the move constructor and move assignment operator, which can be done even if you disable copying).
    If you don't do this, you could end up with double deletion after copy assignment.

    I haven't implemented the copy assignment operator yet but I will do it soon. I've been a little preoccupied by other subjects at school and don't get much time thinking and writing actual code. It's just playing around with different things and learning more about implementation small steps at a time.

    Thanks for your help @laserlight!

Popular pages Recent additions subscribe to a feed

Similar Threads

  1. Replies: 2
    Last Post: 04-13-2017, 01:29 AM
  2. Replies: 2
    Last Post: 05-16-2012, 06:08 AM
  3. Table mapping Strings to Strings
    By johnmcg in forum C Programming
    Replies: 4
    Last Post: 09-05-2003, 11:04 AM

Tags for this Thread