2
\$\begingroup\$

In our apps we're using a shared inhouse library which provides filesystem functions. All the functions are noexcept.

In several apps i found that similar or identical error return translations are implemented again and again [which are then reported on a console or some windows app] and was wondering

  • if it might be a good design to provide default error strings which are transported with exceptions as part of a additional function in the library
  • or if each of these functions should be wrapped in the respective application because the error strings might need application specific context (in this case they don't in their current implementation)
  • or if i should stick with the existing function and reimplement the translation on every invocation (which is how it's implemented currently)
  • or generally what's the best way to approach this.

The code is windows only and not planned to be ported to other platforms. The existing functions in the library can't be changed but it's possible to add new functions.

Here's the code: (Live Demo)

The new function createDirectory_CanThrow is part of the library and provides default translations of the error codes.

#include <cassert>
#include <iostream>
#include <string_view>
#include <windows.h>

namespace myorg::filesystem {
    // cannot change this enum
    enum class CreateDirectoryResult {
        Ok = 0,
        See_GetLastError, // windows specific; use ::GetLastError() to get the reason
        CollidesWithAFileHavingTheSameName,
    };
    // cannot change this method
    CreateDirectoryResult createDirectory(std::string_view directory) noexcept {
        // ...
        return CreateDirectoryResult::Ok;
    }

    // is this good design?
    // reimplement it per app?
    // repeat it on every invocation and don't create a wrapper?
    void createDirectory_CanThrow(std::string_view directory) {
        switch (createDirectory(directory))
        {
            default:
                assert(false);
                [[fallthrough]];
            case CreateDirectoryResult::Ok:
                break;
            case CreateDirectoryResult::CollidesWithAFileHavingTheSameName:
                throw std::runtime_error("Cannot create the directory because its name collides with a file with the same name.");
            case CreateDirectoryResult::See_GetLastError: {
                LPVOID buf;
                ::FormatMessageA(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, (LPCVOID)::GetLastError(), 0, 0, (LPSTR)&buf, 0, NULL);
                std::string msg = (char const*)buf;
                ::LocalFree(buf);
                throw std::runtime_error(msg);
            }
        }
    }
}

int main()
{
    try
    {
        myorg::filesystem::createDirectory_CanThrow("C:\\test");
    }
    catch (std::runtime_error const& e)
    {
        std::cout << "Error creating directory: " << e.what() << "\n";
    }
}
\$\endgroup\$
2
  • 1
    \$\begingroup\$ Please do not edit the question, especially the code, after an answer has been posted. Changing the question may cause answer invalidation. Everyone needs to be able to see what the reviewer was referring to. What to do after the question has been answered. I am not going to roll back the edit in this case, but please be aware of this going forward. \$\endgroup\$
    – pacmaninbw
    Commented Jan 10 at 17:53
  • \$\begingroup\$ @pacmaninbw Thanks for pointing this out. I think in this case G. Sliepen read the code as if the fixing edit has already been made, maybe even without noticing, because otherwise the example wouldn't have made any sense. \$\endgroup\$ Commented Jan 10 at 17:59

3 Answers 3

5
\$\begingroup\$

Alternatives to exceptions

There are various ways to approach it. You don't necessarily need exceptions to ensure your code stays DRY. For example, if you just want to get an error message to print, you could just write something that takes a CreateDirectoryResult as input and returns a std::string. Let's call it what(), it's not a great name but it's to show the equivalence with your exception-handling code:

std::string what(CreateDirectoryResult result) {
    using enum CreateDirectoryResult;

    switch(result) {
        …
        case CollidesWithAFileHavingTheSameName:
            return "Cannot create the directory because its name collides with a file with the same name.";
        …
    }
}

Then in main() you can write:

auto result = myorg::filesystem::createDirectory("C:\\test");

if (result != CreateDirectoryResult::Ok) {
    std::cerr << "Error creating directory: " << what(result) << "\n";
}

Or maybe you can add a check() function that does the printing, and returns a bool so you can also easily use it to decide how to continue after an error:

[[nodiscard]] bool check(CreateDirectoryResult result) {
    if (result != CreateDirectoryResult::Ok) {
        std::cerr << "Error creating directory: " << what(result) << "\n";
        return false;
    } else {
        return true;
    }
}

The [[nodiscard]] attribute will ensure the compiler warns if you ever call check() but don't look at its return value. So then you can write:

auto result = myorg::filesystem::createDirectory("C:\\test");

if (!check(result)) {
    return EXIT_FAILURE;
}

The advantage of exceptions of course is that errors won't be ignored if you forget to check for them, and you can also do multiple actions in a single try-catch block without having to check the result of each individual action.

Make it easy to wrap lots of functions

Your createDirectory_CanThrow() has a lot of code in it. What if you want to wrap another function that also returns a CreateDirectoryResult? The what() function I wrote might be helpful then. You can also make a function like check() that throws the exception for you:

void check(CreateDirectoryResult result) {
    if (result != CreateDirectoryResult::Ok) {
        throw std::runtime_error("Error creating directory: " + what(result) + "\n");
    }
}

So then you can write:

void createDirectory_CanThrow(std::string_view directory) {
    check(createDirectory(directory));
}

You can also consider creating a template that can wrap any function:

template<auto Function>
class MakeThrow {
public:
    template<typename... Args>
    void operator()(Args&&... args) const {
        check(Function(std::forward<Args>(args)...));
    }
};

This is a bit advanced, but it allows you to write:

auto createDirectory_CanThrow = MakeThrow<createDirectory>{};

It still needs you to write overloads for check() that handle all the possible return types that your non-throwing functions can return, but creating the wrapper function is now trivial.

Alternatively, you can create a preprocessor macro that creates wrapped functions. You can make it so you only have to write:

WRAP(createDirectory)

To have it define the createDirectory_CanThrow function that you want. Personally I would try to avoid macros where possible, but here it is one of the DRYest solutions if you have to wrap many functions. Note that the macro doesn't need to do much, it can still rely on helper functions like what(), check(), and the MakeThrow template, for example:

#define WRAP(function) auto function ## _CanThrow = MakeThrow<function>{};
\$\endgroup\$
2
  • \$\begingroup\$ I like a lot of things here. I found two points which i tried to further improve. What do you think about these changes? what returns an optional now to avoid having to return an empty string and further delegate the ok condition. It also takes the actual function as a second argument so that the messages can be based on the concrete function to cover "What if you want to wrap another function that also returns a CreateDirectoryResult" for cases where (maybe just slightly) different messages would make more sense. \$\endgroup\$ Commented Jan 10 at 18:20
  • 1
    \$\begingroup\$ Nice, but if you want to have it reviewed, make a new question here on Code Review :) \$\endgroup\$
    – G. Sliepen
    Commented Jan 10 at 20:25
5
\$\begingroup\$

Let's look at the switch, and in particular, the default case:

    switch (createDirectory(directory))
    {
        default:
            assert(false);
            [[fallthrough]];
        case CreateDirectoryResult::Ok:
            break;
        case CreateDirectoryResult::CollidesWithAFileHavingTheSameName:
            throw ……;
        case CreateDirectoryResult::See_GetLastError:
            throw ……;
    }

If we get an error not mentioned in the function's contract then the assertion will fail, if it's a debug build. But in production builds, where assertions are disabled, we'll fall through to the "okay" case and give no information back to the caller.

A more defensive strategy would be to throw a runtime exception when an unexpected result is encountered (e.g. when the library providing createDirectory() is updated).

Good compilers can help to ensure that we handle all values of the enum, but only if we don't have that catch-all default:

        switch (createDirectory(directory))
        {
            case CreateDirectoryResult::Ok:
                return;
            case CreateDirectoryResult::CollidesWithAFileHavingTheSameName:
                throw ……;
            case CreateDirectoryResult::See_GetLastError:
                throw ……;
        }

        assert(false);  // invalid result
        throw std::runtime_error{"Unhandled error from createDirectory()"};

\$\endgroup\$
1
  • \$\begingroup\$ Good point! I agree with the defensiveness. But i think a lot of people would disagree? F.e. some answers to this question. Couldn't it be argued that whenever a library is updated tests which cover these situations need to be run. \$\endgroup\$ Commented Jan 11 at 12:28
3
\$\begingroup\$

Consider using std::system_error to consolidate the different types of error, rather than manually creating the error message yourself. You can still use .what() to get the error message, as in the original.

namespace myorg::wrapper {

    void createDirectory(std::string_view dir) {
        namespace fs = myorg::filesystem;

        auto result = fs::createDirectory(dir);

        if (result != fs::CreateDirectoryResult::Ok) {

            if (result == fs::CreateDirectoryResult::CollidesWithAFileHavingTheSameName) {
                throw std::system_error(std::error_code(ERROR_FILE_EXISTS, std::system_category())); 
            }

            throw std::system_error(std::error_code(::GetLastError(), std::system_category()));
        }
    }
}
\$\endgroup\$
2
  • \$\begingroup\$ I've never used system_error before but found this answer stating: "For a truly portable and robust solution I would recommend subclassing both std::system_error and std::system_category to windows_error and windows_category and implement the correct functionality yourself". Would it make sense to go that route or isn't it needed anymore? I don't have to support VS < 2022. \$\endgroup\$ Commented Jan 11 at 12:19
  • \$\begingroup\$ I've not had any problems without doing subclassing (with modern MSVC versions). I'd suggest manually throwing a few system_errors with error codes you're interested in, and checking that the messages are sensible. \$\endgroup\$
    – user673679
    Commented Jan 11 at 15:32

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.