What makes this usage of pointers unpredictable?

The behaviour of the program is non-existent, because it is ill-formed.

char* s = "My String";

This is illegal. Prior to 2011, it had been deprecated for 12 years.

The correct line is:

const char* s = "My String";

Other than that, the program is fine. Your professor should drink less whiskey!


The answer is: it depends on what C++ standard you're compiling against. All the code is perfectly well-formed across all standards‡ with the exception of this line:

char * s = "My String";

Now, the string literal has type const char[10] and we're trying to initialize a non-const pointer to it. For all other types other than the char family of string literals, such an initialization was always illegal. For example:

const int arr[] = {1};
int *p = arr; // nope!

However, in pre-C++11, for string literals, there was an exception in §4.2/2:

A string literal (2.13.4) that is not a wide string literal can be converted to an rvalue of type “pointer to char”; [...]. In either case, the result is a pointer to the first element of the array. This conversion is considered only when there is an explicit appropriate pointer target type, and not when there is a general need to convert from an lvalue to an rvalue. [Note: this conversion is deprecated. See Annex D. ]

So in C++03, the code is perfectly fine (though deprecated), and has clear, predictable behavior.

In C++11, that block does not exist - there is no such exception for string literals converted to char*, and so the code is just as ill-formed as the int* example I just provided. The compiler is obligated to issue a diagnostic, and ideally in cases such as this that are clear violations of the C++ type system, we would expect a good compiler to not just be conforming in this regard (e.g. by issuing a warning) but to fail outright.

The code should ideally not compile - but does on both gcc and clang (I assume because there's probably lots of code out there that would be broken with little gain, despite this type system hole being deprecated for over a decade). The code is ill-formed, and thus it does not make sense to reason about what the behavior of the code might be. But considering this specific case and the history of it being previously allowed, I do not believe it to be an unreasonable stretch to interpret the resulting code as if it were an implicit const_cast, something like:

const int arr[] = {1};
int *p = const_cast<int*>(arr); // OK, technically

With that, the rest of the program is perfectly fine, as you never actually touch s again. Reading a created-const object via a non-const pointer is perfectly OK. Writing a created-const object via such a pointer is undefined behavior:

std::cout << *p; // fine, prints 1
*p = 5;          // will compile, but undefined behavior, which
                 // certainly qualifies as "unpredictable"

As there is no modification via s anywhere in your code, the program is fine in C++03, should fail to compile in C++11 but does anyway - and given that the compilers allow it, there's still no undefined behavior in it†. With allowances that the compilers are still [incorrectly] interpreting the C++03 rules, I see nothing that would lead to "unpredictable" behavior. Write to s though, and all bets are off. In both C++03 and C++11.


†Though, again, by definition ill-formed code yields no expectation of reasonable behavior
‡Except not, see Matt McNabb's answer

Other answers have covered that this program is ill-formed in C++11 due to the assignment of a const char array to a char *.

However the program was ill-formed prior to C++11 also.

The operator<< overloads are in <ostream>. The requirement for iostream to include ostream was added in C++11.

Historically, most implementations had iostream include ostream anyway, perhaps for ease of implementation or perhaps in order to provide a better QoI.

But it would be conforming for iostream to only define the ostream class without defining the operator<< overloads.