An invalid default value is basically a variant in your design. The object isn't valid when it's created. You should avoid that when it's reasonable. By no means should you avoid it "at all costs."
Some problems require you to start in a variant state. In that case, you have to reason about that invalid value mentally. If you avoid naming it you are actively making your code less expressive. Think about it in terms of communication between you and the person that will have to maintain the code later.
Dealing with it downwind is annoying. You start in a variant state, but by the time it's relevant you hope that it is no longer variant. The strategy I prefer is allow users to ignore the variant state and just throw when I've made a mistake.
namespace FooType {
enum EnumValue {
INVALID = 0
,valid
};
}
struct Foo {
Foo() : val(FooType::INVALID) {}
FooType::EnumValue get() const {
if (val == FooType::INVALID)
throw std::logic_error("variant Foo state");
return val;
}
FooType::EnumValue val;
};
This frees your users from having to reason about your variance, which is worth fighting for.
If you can't get away with that, I usually prefer to degrade to safe and unsafe interfaces.
struct Foo {
Foo() : val(FooType::INVALID) {}
bool get(FooType::EnumValue& val_) const {
if (val == FooType::INVALID)
return false;
val_ = val;
return true;
}
FooType::EnumValue get() const {
FooType::EnumValue val_;
if (!get(val_))
throw std::logic_error("variant Foo state");
return val_;
}
FooType::EnumValue get_or_default(FooType::EnumValue def) const {
FooType::EnumValue val_;
if (!get(val_))
return def;
return val_;
}
FooType::EnumValue val;
};
These sorts of interfaces are good for things like databases, where null values may be expected.