Is using a wrapper with a 'check' function a good approach to optional types rather than the traditional method?

80 Views Asked by At

languages like C++ and Rust have an Option type, which is basically an enum/bool with a value.

But I feel like this can have a few problems like:

  • the extra overhead of returning/passing an extra field with your type
  • breaking ABI compatibility if you decide that a function should return an optional instead of the raw value.

So why not instead of putting a bool with the value, have a function associated with the type to check if the value returned is valid or not.

for example:

template<typename T, bool(*ISOK)(T&)>
struct Errorable
{
    private:
    T val;

    public:
    Errorable(T v)
    {
        val = v;
    }
    
    bool is_ok()
    {
        return ISOK(val);
    }

    bool is_err()
    {
        return !ISOK(val);
    }

    T unwrap_or(T o)
    {
        if(ISOK(val))
            return val;
        return o;
    };

    T unwrap_forced()
    {
        return val;
    }
};

which can be used like:

bool idx_ok(int& idx)
{
    return idx >= 0;
}

Errorable<int, idx_ok> array_idx_of(int *arr, int len, int elm)
{
    for(int i = 0 ; i < len ; i++)
    {
        if(arr[i] == elm)
        {
            Errorable<int, idx_ok> ok(i);
            return ok;
        }
    }
    Errorable<int, idx_ok> err(-1);
    return err;
}

and then

int main()
{
    int arr[5] = {1, 2, 3, 4, 5};
    auto idx = array_index_of(arr, 5, 6);
    if(idx.is_ok())
    { ... }
}

sorry if my C++ is bad, I'm a C programmer who needed templates for a sec

2

There are 2 best solutions below

2
Miles Budnek On BEST ANSWER

The approach you gave requires that the returned object has some sort of invalid state, but not all objects have such a state.

As an example, the C function atoi converts a C-style string to an int, and returns 0 if it fails. The problem is that 0 is also a valid integer. Since there's no value of of an int that can't be the result of successful conversion there's no way to tell just from looking at the return value if atoi failed or not.

Your Errorable has the same problem. For instance, if you were to try to write your own atoi using Errorable you couldn't do any better than the existing implementation.

The value of std::optional (and Option types in general) is that they avoid this problem by decoupling the existence of the object from the value of the object. You could write a std::optional<int> my_atoi(const char*) that can return any value in the entire range of an int and allows you to determine if the function failed to process its input or not.

0
pptaszni On

If your function can either return a value, or "no value", then feel free to use std::optional:

std::optional<int> tryToGetValue() {
  if (condition) return 100;
  else return std::nullopt;
}

If your function always returns some value/structure, however that structure invariants are not always satisfied, return a structure with validation function (you can generalize it to provide custom validators, use polymorphism etc.):

struct JsonResponse {
  bool isValid();
  int getValue(/* some args */);
  // some logic
};
JsonResponse requestReply(/* ... */);

If your function should always return a value, and it doesn't make sense to continue with the rest of your program logic if that value is invalid, then throw the exception:

int getElement(int idx) {
  if (container.size() <= idx) throw std::logic_error("idx out of bounds");
}

Try to always prioritize the semantics, and write the code that expresses the intent. Then later (only for experts!) you can try to profile it and optimize.