In my experience, its best to rephrase the idea of "assume s->str is a valid malloc'd string" into one of the phrasings which captures not just the assumption, but what happens if it fails.
The weakest notation I'd recommend is using the term "preconditions." Preconditions are things that must be met in order for the algorithm to work. Saying "Preconditions: s->str is a valid malloc'd string" is the formal way of saying that you're assuming it to be true. Specifically it states that you're really not considering what happens if s->str is not a valid string. If you use "assume," it may be ambiguous (with the different meanings below), but preconditions is a formal phrasing with a well understood meaning.
If we want to go further, the next step is to try to explain what happens if the pre-conditions are not met when the function is called. Fortunately, there are some accepted wordings that can be used for this. The first on the list is "undefined behavior." This is the nastiest of these formal wordings. If I state that "if s->str is not a valid string allocated with malloc() the behavior of the function is undefined," that has a clear meaning of "absolutely anything can happen." In your case, this is likely the behavior you will see. If you access memory that isn't allocated, C++ calls that undefined behavior. Undefined behavior is really bad news. It can cause any effect, including affecting completely unrelated code (such as writing data over the top of other functions' data). If it crashes when you do, that means you got lucky. If it gives you wrong answers several seconds later in an unrelated function, it may take a long time to figure out what happened.
A kinder guarantee you can offer is "unspecified" behaviors. Unspecified behaviors are those which aren't reliable, but will always at least be limited to a known set. A function that returns an "unspecified integer if s->len is greater than 1 million" can return any integer in that case, but the rest of the behaviors are understood. Unspecified behavior won't create unexpected surprises in other sections of code... they'll just give an unspecified result.
The most well known use of unspecified behavior in C++ I am aware of is the order of evaluation of arguments. In f(g(), h())
, it is unspecified what order g
and h
are executed in. But you know for certain that either g
is executed and then h
, or h
is executed and then g
, and you can plan accordingly. In my own programming experience, I've often used unspecified results to describe what happens when operating on something with 0 elements. Lots of algorithms are well defined for a non-zero number of elements, like a f(1)
or f(15)
, but there just isn't a really good answer for what should happen for f(0)
because the math just sort of breaks down. In these cases, I will often elect to say "the result of f(0) is unspecified," and let it be whatever behavior my implementation happens to have (as long as it doesn't invoke any undefined behavior).
One step nicer than that is "implementation defined behavior." This is a less common phrasing. Undefined behavior and unspecified behavior are common phrasings. Implementation defined behavior is less common. As the C++ spec uses it, implementation defined behavior is behavior which is unspecified by the spec, but any given implementation is expected to provide a definition that you can look up. If my above f(0)
example was "implementation defined" rather than "unspecified," the result would be the same except that I would be obliged to provide a document that states what value I chose for f(0)
. In some cases that is easy. In other cases that's hard.
The final level of definition is to actually specify what happens when a particular assumption is violated. In your example case, this may be hard to do, because there's not many things your algorithm can do if s->str is not a valid string which don't become undefined. However, in many cases you can pick a behavior, and then constrain yourself to it.
So what should you do? The answer depends on how the program will be used. But its worth recognizing that these classes of behavior are generally transitive. If I call a function with undefined behavior, the result of my function is undefined, and so on and so forth all the way up to the program -- we say the program has undefined behavior. Likewise, calling a function with an unspecified value typically results in my result being unspecified as well.
So what behavior is acceptable for a program? If this program is just trying to get a grade in a computer science course, even undefined behavior might be acceptable. But if you're writing an industrial application, undefined behavior can be very bad. Undefined behavior in a robot controller can result in someone being killed. So in an industrial setting, those behaviors aren't accepted. This means that anyone using your function must take the time to prove that your function will not cause undefined behavior when called with their arguments.
This can be expensive. As a result, one may wish to redefine the assumptions. I have written code where init_str
not only initializes the string, but creates a note in my own data structures that that particular block of memory was initialized with init_str
. do_something
then checks to make sure that data was actually allocated before accessing the data in the string. If the check fails, it does something specified or unspecified (such as just returning 0)
In some industries, unspecified behavior is not acceptable. In air traffic control, many applications must be deterministic. All behaviors must be specified.
In all situations, the key to answering your question "should I check" is three fold:
- Can I check for the condition? If I can't, then obviously I can't do anything about it.
- Is it affordable to check for the condition? Sometimes I can check, but it takes a ton of CPU time to do it.
- How does it affect the users of my function?
The last one is really key. If you give me a function with undefined behaviors, I have to grapple with that. I may have to be able to develop a proof that my particular use of the function is always defined. That can be so difficult that I may adjust my algorithm just to make the proof easier.
len
argument toinit_str
and thelen
element ofstring
assize_t
to guarantee that they cannot hold an invalid value. (As an aside,sizeof(char)
is defined to equal 1, so you can just saymalloc(len)
.)sizeof(char)
is always 1 (see e.g. en.cppreference.com/w/c/language/…), butchar
doesn't need to be 8 bits.