Often when writing code I need to check return values before continuing with the operation. This means that the code is cluttered by control-flow that I don't really care about, and that following the happy-flow is harder for readers of the code in the future.
Apart from early returns and nesting if-statements, is there a way I can organize my function's body to make the intent of the happy-path clearer, and draw less attention to the handling of edge-cases?
I can't use exceptions, so relying on the underlying calls throwing isn't a possibility either.
For illustration, here is a function that tries to encode some data, and write it to a file, written in a style with early returns.
bool ComponentVersionDevice::CommitCurrentBaseline() {
FileHandle baseline{(&m_fs), FilesystemPartition_e::CCONFIGURATION_PARTITION};
if (!baseline.OpenFile(CURRENT_BASELINE_LOCATION)) {
return false;
}
JsonEncoder encoder;
m_parseBuffer.clear();
const auto lengthOrErrorCode =
encoder.Encode(m_currentBaseline, m_parseBuffer.begin(), m_parseBuffer.max_size(), OBJECT);
if (lengthOrErrorCode < 0) {
LOGGER.WriteE(LOG_TAG, "Couldn't encode the current baseline. JSON error: [%d]", lengthOrErrorCode);
return false;
}
if (lengthOrErrorCode == PARSE_BUFFER_SIZE || m_parseBuffer.truncated() != 0) {
LOGGER.WriteE(LOG_TAG, "Couldn't encode the current baseline, it's too large for our parsing buffer...");
return false;
}
if (!baseline.Write(m_parseBuffer)) {
LOGGER.WriteE(LOG_TAG, "Couldn't write the encoded currentbaseline to the file.");
return false;
}
return true;
}
Almost the same code written using nested if's:
bool ComponentVersionDevice::CommitCurrentBaseline() {
FileHandle baseline{(&m_fs), FilesystemPartition_e::CCONFIGURATION_PARTITION};
if (baseline.OpenFile(CURRENT_BASELINE_LOCATION)) {
JsonEncoder encoder;
m_parseBuffer.clear();
const auto lengthOrErrorCode =
encoder.Encode(m_currentBaseline, m_parseBuffer.begin(), m_parseBuffer.max_size(), OBJECT);
if(lengthOrErrorCode > 0){
if (!m_parseBuffer.truncated()) {
if (baseline.Write(m_parseBuffer)) {
return true;
} else {
LOGGER.WriteE(LOG_TAG, "Couldn't write to file");
}
} else {
LOGGER.WriteE(LOG_TAG, "Encoded baseline doens't fit in buffer!");
}
} else {
LOGGER.WriteE(LOG_TAG, "Couldn't encode, error: %d", lengthOrErroCode);
}
}
return false;
}
What the actual code does, without error handling:
bool ComponentVersionDevice::CommitCurrentBaseline() {
FileHandle baseline{(&m_fs), FilesystemPartition_e::CCONFIGURATION_PARTITION};
baseline.OpenFile(CURRENT_BASELINE_LOCATION)
JsonEncoder encoder;
m_parseBuffer.clear();
encoder.Encode(m_currentBaseline, m_parseBuffer.begin(), m_parseBuffer.max_size(), OBJECT);
baseline.Write(m_parseBuffer);
return true;
}
Here you can more clearly see what the intent is, and what the function is supposed to do.
Is there another way to chain operations that have dependencies on the previous steps succeeding without having all the error handling interwoven like in the examples?
Some technical constraints given the environment I want to apply this to:
- No exceptions
- Little or preferably no heap usage
- using features available in c++20 or earlier
while(false) { if(!happy) goto hell; ... }
maybe? \$\endgroup\$