2

I am new to C++ programming (work with Java mostly), and this behavior of C++ classes, member strings and string conversions to const char* with c_str() is confusing me.

I have a header, a class and main function as follows:

sample.h

class Sample
{
private:
    int id;
    std::string text;
public:
    Sample(int id);
    void setId(int id);
    int getId();
    void setText(std::string txt);
    std::string getText();
    void loadText();
    ~Sample();    
}

sample.cpp

Sample::Sample(int id)
{
    this->id = id;
}

void Sample::setId(int id)
{
    this->id = id;
}

int Sample::getId()
{
    return this->id;
}

void Sample::setText(std::string txt)
{
    this->text = txt;
}

std::string Sample::getText()
{
    return this->text;
}

void Sample::loadText()
{
    this->text = "Loaded";
}

Sample::~Sample()
{
    std::cout << "Destructor is called." << std::endl;
}

main.cpp

void main()
{
    int id = 1;
    Sample* sample = new Sample(id);

    // Case: 1 - If I do this, it does not work. Prints gibberish.
    sample->loadText();
    const char* text = sample->getText().c_str();
    std::cout << text << std::endl;

    // Case: 2 - Otherwise, this works.
    sample->loadText();
    std::cout << sample->getText().c_str() << std::endl;

    // Case: 3 - Or, this works
    sample->loadText();
    std::string txtCpy = sample->getText();
    const char* text = textCpy.c_str();
    std::cout << text << std::endl;
}

All three cases are done one at a time.

Case 3 does satisfy my use case (which is, passing the string to a C library that expects a const char*. But, I can't figure out the difference between Case: 1 and Case: 3? If we are returning the string by value, how does copying it to an intermediate variable make it kosher for the run-time?

praet
  • 23
  • 2
  • This is undefined behaviour. See here: https://stackoverflow.com/questions/35980664/why-does-calling-stdstring-c-str-on-a-function-that-returns-a-string-not-wor?noredirect=1&lq=1 – Rish Mar 05 '20 at 23:10
  • 1
    [`main` must have return type `int` in C++](https://stackoverflow.com/questions/204476/what-should-main-return-in-c-and-c). [`new` should almost never be used directly in C++.](https://stackoverflow.com/questions/6500313/why-should-c-programmers-minimize-use-of-new) [Don't declare destructors](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-dtor) [if your class is not managing a ressource](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-zero). – walnut Mar 05 '20 at 23:19
  • Also, [don't use assignment in constructors, use initialization instead](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#Rc-initialize). – walnut Mar 05 '20 at 23:26

2 Answers2

2

In this code snippet

const char* text = sample->getText().c_str();
std::cout << text << std::endl;

the variable text is assigned by a pointer (c_str()) of a temporary object returned from the member function getText. After this statement the temporary object will nit be alive, So the variable text has an invalid pointer,

The code snippet could be valid if the member function returned reference to the data member text like for example

const std::string & Sample::getText() const
{
    return this->text;
}

Pay attention to that this declaration of main

void main()

is not a standard declaration.

The standard declaration of main without parameters is

int main()
Vlad from Moscow
  • 224,104
  • 15
  • 141
  • 268
  • Makes sense. Thank you. If it were a primitive, instead of string it would work right? – praet Mar 05 '20 at 23:08
  • 2
    @praet Using a pointer to an object (it is unimportant whether it is primitive or not) after the object is not alive invokes undefined behavior. – Vlad from Moscow Mar 05 '20 at 23:10
  • note: this suggestion does still permit the same sort of error, e.g. `const std::string& foo = sample->getText();` and then using `foo` after the object was destroyed . This could well show up if you ever return a `Sample` from a function. So you still need to be vigilant – M.M Mar 05 '20 at 23:12
2

The result of c_str() is only valid while the string you called it on still exists. In case 3, txtCpy still exists at the point you are writing cout << text. But in Case 1, the string was the return value of sample->getText which is temporary and stop existing at the end of that line .

This issue always will exist if you take pointers or references to other objects. A naked pointer or reference has its own lifetime which may differ from the lifetime of the targeted object. This is unlike Java where object references all participate in the lifetime of the object.

As such, you always need to think about object lifetimes when using these features, and it's commonly recommended to instead use higher level features or other code styles that do not permit lifetime management errors.

You could consider adding a member function to Sample which gets a const char * pointing at the original string (although this is a wee violation of encapsulation, and still has a similar class of problem if you hold onto the pointer and then modify the underlying string). Better would be to just avoid working with the naked pointers entirely.

M.M
  • 130,300
  • 18
  • 171
  • 314