6
\$\begingroup\$

I'm trying out some different programming styles to experiment with the new C++ concepts. Within the experiment I was looking into how constexpr could be helpful for writing unit tests at compile time.

This code represents a single cell in a Sudoku, where this cell is represented as some kind of bitset with true/false values to indicate if the value indicated by the index is still possible in that cell.

Some remarks about the code:

  • This isn't functionally ready to solve a real Sudoku
  • I omitted most comments as I want feedback on the techniques used, not the personal tastes

Some of the elements I'm struggling with:

  • One needs exceptions to force compilation errors on invalid usage at compile time, these checks might not be wanted when using the code in production
  • Making a method with exceptions noexcept doesn't eliminate the checks as an escaping exception is defined behavior and it will call std::terminate.
  • Should the exceptions inherit from std::exception as this makes it possible to catch them?
  • Is it a good idea to recompile/evaluate the unit tests in every compilation unit?
  • I need ugly C-style arrays as std::array doesn't support constexpr at full. The same holds for std::count which needed reimplementation as constexpr support was not yet implemented.

Same code @godbolt.org

#include <array>
#include <algorithm>

#ifdef MAKE_EXCEPTIONS_UNDEFINED_BEHAVIOR
#define NOEXCEPT noexcept
#else
#define NOEXCEPT
#endif

/// Own extensions to the std namespace as not all C++20 changes have been implemented.
namespace extstd
{
    using namespace std;
    template<class InputIt, class T>
    constexpr typename iterator_traits<InputIt>::difference_type count(InputIt first, InputIt last, const T& value)
    {
        typename iterator_traits<InputIt>::difference_type ret = 0;
        for (; first != last; ++first) {
            if (*first == value) {
                ret++;
            }
        }
        return ret;
    }
}

constexpr auto VALUE_UNKNOWN = 0;
constexpr auto NOF_VALUES_DIM1 = 3;
constexpr auto NOF_VALUES_DIM2 = 3;

constexpr auto NOF_VALUES = NOF_VALUES_DIM1 * NOF_VALUES_DIM2;
constexpr auto NOF_CELLS = NOF_VALUES * NOF_VALUES;
static_assert(NOF_CELLS == 81);

template<typename TType, std::size_t TNofValues>
class OneBasedArray final
{
    TType mValues[TNofValues];
    struct InvalidValueException : public std::exception {};

    constexpr static auto checkBoundries(int i) NOEXCEPT
    {
        if (i <= 0)
            throw InvalidValueException{};
        if (i > TNofValues)
            throw InvalidValueException{};          
    }

    constexpr static auto checkIndex(std::size_t index) NOEXCEPT
    {
        if (index >= TNofValues)
            throw InvalidValueException{};
    }

    constexpr static auto toIndex(int index) NOEXCEPT
    {
        checkBoundries(index);
        return static_cast<std::size_t>(index - 1);
    }
    constexpr static auto fromIndex(std::size_t index) NOEXCEPT
    {
        checkIndex(index);
        return static_cast<int>(index + 1);
    }

public:
    constexpr OneBasedArray() : mValues() {};

    constexpr auto begin() { return std::begin(mValues); }
    constexpr auto begin() const { return std::begin(mValues); }
    constexpr auto cbegin() const { return std::begin(mValues); }
    constexpr auto end() { return std::end(mValues); }
    constexpr auto end() const { return std::end(mValues); }
    constexpr auto cend() const { return std::end(mValues); }

    constexpr auto operator[](int i) -> TType & { return mValues[toIndex(i)]; }
    constexpr auto operator[](int i) const -> const TType & { return mValues[toIndex(i)]; }

    constexpr auto fill(const TType &v) -> void
    {
        for (std::size_t i = 0; i < TNofValues; ++i)
         mValues[i] = v;
    }
};

class Cell final
{
    OneBasedArray<bool, NOF_VALUES> mOptions;

    struct InvalidValueException : public std::exception {};
    struct IncorrectCallingSequenceException : public std::exception {};

    constexpr auto validateIncomplete() const NOEXCEPT
    {
        if (isCompletelyKnown())
            throw IncorrectCallingSequenceException{};          
    }


public:
    constexpr Cell() { mOptions.fill(true); }

    constexpr auto isCompletelyKnown() const -> bool { return extstd::count(std::cbegin(mOptions), std::cend(mOptions), true) == 1; }

    constexpr auto getValue() const -> int
    {
        if (!isCompletelyKnown())
            return VALUE_UNKNOWN;

        //TODO
    }

    constexpr auto setToValue(int i) NOEXCEPT -> void
    {
        validateIncomplete();

        if (mOptions[i] == false)
            throw IncorrectCallingSequenceException{};

        mOptions.fill(false);
        mOptions[i] = true;
    }

    constexpr auto removeOption(int i) NOEXCEPT -> void
    {           
        if (mOptions[i] == false)
            return; // already OK

        validateIncomplete();
        mOptions[i] = false;
    }
};

constexpr auto unittestCellRemoveOption() -> bool
{
    Cell c{};

    for (int i = 1; i < NOF_VALUES; ++i)
    {
        if (c.isCompletelyKnown())
            return false;
        c.removeOption(i);
    }
    if (!c.isCompletelyKnown())
        return false;

    return true;
}
static_assert(unittestCellRemoveOption());

constexpr auto unittestCellSetValue() -> bool
{
    Cell c{};
    if (c.isCompletelyKnown())
        return false;
    c.setToValue(3);
    if (!c.isCompletelyKnown())
        return false;
    return true;
}
static_assert(unittestCellSetValue());
\$\endgroup\$
3
\$\begingroup\$

I've also been messing around with making everything constexpr, then using static_asserts in addition to regular unit tests to find problems before even running the tests. I haven't really gone as deep into it as you have, though.

I'm going to focus specifically on the elements you say you're struggling with.

The problem with needing exceptions

What you really want, I think, is the ability to use a different function implementation at compile time versus run time. That's been something people have been asking for since constexpr restrictions were relaxed for C++14. There have been multiple proposals going back to 2013, with such syntaxes as a constexpr() operator, constexpr function parameters, and the very shouty constexpr!.

Unfortunately, these are all just proposals, so there's no way to get what you really want in practice.

One option to get partway there is to use some kind of assert macro. Something like:

// Obviously give these better names
#ifdef NDEBUG
#   define ASSERT(x) ((void)0)
#   define ASSERT_MSG(x, msg) ((void)0)
#else
#   define ASSERT(x) your_assert((x), #x)
#   define ASSERT_MSG(x, msg) your_assert((x), (msg))

// could be noexcept if you want to terminate, which makes sense for an
// assertion failure
inline constexpr auto your_assert(bool okay, std::string_view message)
{
    if (!okay)
        throw std::logic_error{message};
}

#endif // NDEBUG

Then you could write the assertions you only want during testing as:

constexpr static auto checkBoundries(int i) noexcept
{
    ASSERT(i <= 0);
    ASSERT(i > TNofValues);
}

So what would happen is during testing you'd get your compile time tests, and for problems that only show up at run time, you get assertion failures (ie, terminations). Once your testing cycle is complete, you could compile with -DNDEBUG, and the assertions are all removed just as with regular assert().

It's not perfect, and I'm the last person to encourage the use of the preprocessor or multiple compilation modes, but it's probably the best you can do for now. It will get you compile-time testing when NDEBUG isn't defined, and zero run-time cost when NDEBUG is.

If the NDEBUG thing really bugs you - and it bugs me - then I suppose the best you could do is mark the assertion failure [[unlikely]] (using whatever macro you prefer), and hope the branch predictor helps keep the cost of the checks down to negligible.

noexcept and terminate

I don't really see this as a problem as you are talking about assertion failures - you're talking about things that shouldn't even compile, but just don't happen to get detected until run time. If someone is trying to access your array out-of-bounds, then someone seriously screwed up somewhere, and there's no reason to give them the courtesy of letting them screw up a second time. Terminating is a perfectly reasonable thing to do in such a case.

Inheriting from std::exception

It's not worth worrying about if you're only triggering compile errors and/or termination. In fact, you might as well just throw an int for all it matters. Or something like throw "index out of bounds"; to get a readable error message.

Unit tests in every compilation unit

I wouldn't worry about it unless/until compile times are getting problematic.

I generally have a matching .cpp for every .hpp, even though they often turn out to be mostly empty for headers full of templates. If you follow that pattern, you can put the unit tests in the .cpp, and they'll only be run if the code changes (assuming you're using a decent build system), or when you explicitly want them run.

constexpr in the library

I don't know if you're aware, or what version of C++ you're working with (the question is currently tagged C++ 20, but I gather there's some discussion about that - it seems at least your code requires C++17, because you require constexpr std::begin() and so on), but std::array is totally constexpr as of C++17.

count() isn't constexpr yet, unfortunately, but should be for C++20.

\$\endgroup\$
  • \$\begingroup\$ My problem with the std::terminate ain't about it killing the process, it's about the generated code for the if-statement \$\endgroup\$ – JVApen May 28 '18 at 16:15
  • \$\begingroup\$ Yeah, what you really want is a way to have different functions at run time and compile time. But there's just no way up to C++17 at least. The macro solution (ie, defining the check out of existence) is pretty much your only option for "perfect" code generation. \$\endgroup\$ – indi May 28 '18 at 16:19
  • \$\begingroup\$ Looks indeed like there is a design flaw of requiring exceptions as an alternative to assert (Which again uses a macro) \$\endgroup\$ – JVApen May 28 '18 at 16:26

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

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