1

I like to know pro's and con's for having and not-having such cast. At several places including here on Stack Overflow I can see that the const char* cast is considered bad idea but I am not sure why?

Lack of the (const char*) and forcing to always use c_str() cast creates some problems when writing generic routines and templates.

void CheckStr(const char* s)
{
}

int main()
{
    std::string s = "Hello World!";

    // all below will not compile with 
    // Error: No suitable conversion function from "std::string" to "const char *" exists!
    //CheckStr(s);             
    //CheckStr((const char*)s);  
    // strlen(s);

    // the only way that works
    CheckStr(s.c_str());
    size_t n = strlen(s.c_str());
    return 0;
}

For example, if I have a large number of text processing functions that accept const char* as input and I want to be able to use std::string each time I have to use c_str(). But in this way a template function can't be used for both std::string and const char* without additional efforts.

As a problem I can see some operator overloading issues but these are possible to solve.

For example, as [eerorika] pointed, with allowing implicit cast to pointer we are allowing involuntary the string class to be involved in boolean expressions. But we can easily solve this with deleting the bool operator. Even further, the cast operator can be forced to be explicit:

class String
{
public:
    String() {}
    String(const char* s) { m_str = s; }
    const char* str() const  { return m_str.c_str(); }
    char* str()  { return &m_str[0]; }
    char operator[](int pos) const { return m_str[pos]; }
    char& operator[](int pos) { return m_str[pos]; }
    explicit operator const char*() const { return str(); }  // cast operator
    operator bool() const = delete;

protected:
    std::string m_str;
};

int main()
{
    String s = "Hello";
    string s2 = "Hello";
    if(s)  // will not compile:  it is a deleted function
    {
        cout << "Bool is allowed " << endl;
    }

    CheckStr((const char*)s);
    return 0;
}
Baj Mile
  • 750
  • 1
  • 8
  • 17
  • If there is to be a conversion operator from `std::string` to `const char*`, what would the lifetime of the resulting array of `char` be? – ph3rin Nov 27 '19 at 18:04
  • ...And if your string check methods are not from some 3rd-party libraries, then pass a `const std::string&` instead. – ph3rin Nov 27 '19 at 18:08
  • @KaenbyouRin: Why couldn't it be the same lifetime as for `.c_str()`? – Nate Eldredge Nov 27 '19 at 18:08
  • *I like to know pro's and con's for having and not-having such cast.* -- You say that "these issues are possible to solve", [but at the cost of unexpected surprises](https://stackoverflow.com/questions/492061/why-doesnt-stdstring-provide-implicit-conversion-to-char) – PaulMcKenzie Nov 27 '19 at 18:16

2 Answers2

6

I like to know pro's and con's for having and not-having such cast.

Con: Implicit conversions often have behaviour that is surprising to the programmer.

For example, what would you expect from following program?

std::string some_string = "";
if (some_string)
    std::cout << "true";
else
    std::cout << "false";

Should the program be ill-formed because std::string is has no conversion to bool? Should the result depend on the content of the string? Would most programmers have the same expectation?

With the current std::string, the above would be ill-formed because there is no such conversion. This is good. Whatever the programmer expected, they'll find out their misunderstanding when they attempt to compile.

If std::string had a conversion to a pointer, then there would also be a conversion sequence to bool through the conversion to pointer. The above program would be well-formed. And the program would print true regardless of the content of the string, since c_str is never null. What if programmer instead expected that empty string would be false? What if they never intended either behaviour, but used a string there by accident?

What about the following program?

std::string some_string = "";
std::cout << some_string + 42;

Would you expect the program to be ill-formed because there is no such operator for string and int?

If there was implicit conversion to char*, the above would have undefined behaviour because it does pointer arithmetic and accesses the string buffer outside of its bounds.


// all below will not compile with 
strlen(s);

This is actually a good thing. Most of the time, you don't want to call strlen(s). Usually, you should use s.size() because it is asymptotically faster. The need for strlen(s.c_str()) is so rare, that the little bit of verbosity is insignificant.

Forcing the use of .c_str() is great because it shows the reader of the program that it is not a std::string that is passed to the function / operator, but a char*. With implicit conversion, it is not possible to distinguish one from the other.


... creates some problems when writing generic routines and templates.

Such problems are not insurmountable.

eerorika
  • 232,697
  • 12
  • 197
  • 326
  • And this doesn't even get into the issues of lifetime, since the resulting pointer is only valid while that particular string exists. With implicit conversions, it becomes a bit easier to violate the lifetime problems. – Nicol Bolas Nov 27 '19 at 18:33
  • Good points. But would you like to discuss it? You first example can be resolved with deleting bool operator. I will put the code in the question. – Baj Mile Nov 27 '19 at 19:23
  • @BajMile: This is not a forum; we don't do discussion here. – Nicol Bolas Nov 27 '19 at 20:26
  • @BajMile There is a chat for discussion: https://chat.stackoverflow.com/rooms/10/loungec (but there is some sort of minimal rep requirement) – eerorika Nov 27 '19 at 20:34
4

If by "having a cast" you mean a user defined conversion operator, then the reason it does not have it is: to prevent you from using it implicitly, possibly inadvertently.

Historically, unpleasant consequences of an inadvertent use of such conversion stem the fact that in the original std::string (per C++98 specification) the operation was heavy and dangerous.

  • The original std::string was not trivially convertible to const char *, since the string object was not originally intended/required to store a null-terminator character. Under those circumstances, conversion to const char * was a potentially heavy operation that generally allocated an independent buffer and copied the entire controlled sequence to that buffer.

  • The independent buffer mentioned above (if used) had potentially "unexpected" lifetime. Any modifying operation on the original std::string object triggered invalidation/deallocation of that buffer, rendering previously returned pointers invalid.

It is never a good idea to implement such heavy and dangerous operations as implicitly-invokable conversion operators.

The original C++ standard (C++98) did not have such feature as explicit conversion operators. (They first appeared in C++11.) A dedicated named member function was the only way to somehow make the conversion explicit in C++98.

Today, in modern C++, we can define a conversion operator and still prevent it from being used implicitly (by using explicit keyword). One can argue that under such circumstances implementing the conversion by an operator is a reasonable approach. But I'd still argue that it is not a good idea. Even though the modern std::string is required to store its null-terminator (i.e. c_str() no longer produces an independent buffer), the pointer returned by the conversion to const char * is still "dangerous": many modification operations applied to std::string object may (and will) invalidate this pointer. To emphasize the fact that this is not a mere safe and innocent conversion, but rather an operation that produces a potentially dangerous pointer, it is quite reasonable to implement it by a named function.

AnT stands with Russia
  • 312,472
  • 42
  • 525
  • 765
  • Thanks for sharing this. Things has changed since C++98, we can also argue why the class has to be responsible that someone is holding invalid pointers to part of its content. This is responsibility of the user of the class. With c_str() access to internal buffer.is anyway exported. – Baj Mile Nov 27 '19 at 19:46
  • @BajMile Because conversion is idiomatically expected to yield a "deep copy" of the original object (converted of course), not a view on the original object (like `std::string_view`). – ph3rin Nov 27 '19 at 22:09