Consider the following piece of C++ code:
void broken()
{
int i = rand() % 10;
if (i == 0) // 1 in 10 chance.
goto iHaveABadFeelingAboutThis;
std::string cake = "a lie";
// ...
// lots of code that prepares the cake
// ...
iHaveABadFeelingAboutThis:
// 1 time out of ten, the cake really is a lie.
eat(cake);
// maybe this is where "iHaveABadFeelingAboutThis" was supposed to be?
std::cout << "Thank you for calling" << std::endl;
}
Ultimately, "goto" is not much different than C++'s other flow-control keywords: "break", "continue", "throw", etc; functionally it introduces some scope-related issues as demonstrated above.
Relying on goto will teach you bad habits that produce difficult to read, difficult to debug and difficult to maintain code, and it will generally tend to lead to bugs. Why? Because goto is free-form in the worst possible way, and it lets you bypass structural controls built into the language, such as scope rules, etc.
Few of the alternatives are particularly intuitive, and some of them are arguably as ambiguous as "goto", but at least you are operating within the structure of the language - referring back to the above sample, it's much harder to do what we did in the above example with anything but goto (of course, you can still shoot yourself in the foot with for/while/throw when working with pointers).
Your options for avoiding it and using the language's natural flow control constructs to keep code humanly readable and maintainable:
- Break your code up into subroutines.
Don't be afraid of small, discrete, well-named functions, as long as you are not perpetually hauling a massive list of arguments around (if you are, then you probably want to look at encapsulating with a class).
Many novices use "goto" because they write ridiculously long functions and then find that they want to get from line 2 of a 3000 line function to line 2998. In the above code, the bug created by goto is much harder to create if you split the function into two payloads, the logic and the functional.
void haveCake() {
std::string cake = "a lie";
// ...
// lots of code that prepares the cake
// ...
eat(cake);
}
void foo() {
int i = rand() % 10;
if (i != 0) // 9 times out of 10
haveCake();
std::cout << "Thanks for calling" << std::endl;
}
Some folks refer to this as "hoisting" (I hoisted everything that needed to be scoped with 'cake' into the haveCake function).
These are not always obvious to programmers starting out, it says it's a for/while/do loop but it's actually only intended to run once.
for ( ; ; ) { // 1-shot for loop.
int i = rand() % 10;
if (i == 0) // 1 time in 10
break;
std::string cake = "a lie";
// << all the cakey goodness.
// And here's the weakness of this approach.
// If you don't "break" you may create an infinite loop.
break;
}
std::cout << "Thanks for calling" << std::endl;
These can be very powerful, but they can also require a lot of boiler plate. Plus you can throw exceptions to be caught further back up the call stack or not at all (and exit the program).
struct OutOfLuck {};
try {
int i = rand() % 10;
if (i == 0)
throw OutOfLuck();
std::string cake = "a lie";
// << did you know: cake contains no fat, sugar, salt, calories or chemicals?
if (cake.size() < MIN_CAKE)
throw CakeError("WTF is this? I asked for cake, not muffin");
}
catch (OutOfLuck&) {} // we don't catch CakeError, that's Someone Else's Problem(TM).
std::cout << "Thanks for calling" << std::endl;
Formally, you should try and derive your exceptions from std::exception, but I'm sometimes kind of partial to throwing const char* strings, enums and occasionally struct Rock.
try {
if (creamyGoodness.index() < 11)
throw "Well, heck, we ran out of cream.";
} catch (const char* wkoft /*what kind of fail today*/) {
std::cout << "CAKE FAIL: " << wkoft << std::endl;
throw std::runtime_error(wkoft);
}
The biggest problem here is that exceptions are intended for handling errors as in the second of the two examples immediately above.