Hacker News new | ask | show | jobs
by jstimpfle 977 days ago
It's a funny coincidence that your username is "dataflow" when that is exactly what's broken with default arguments: you can't pass the default values around, they can't flow in the code.

If you want to create a proxy function to a function that has default arguments, and want to transparently allow the "default" features to be used from the wrapper function as well, then you have to duplicate the default value in the signature of the wrapper function.

There are other problems, for example due to the nature of function call syntax with positional arguments.

The solution is: Use a struct to hold default values.

    struct FooDefaults
    {
        int arg1 = 3;
        int arg2 = 7;
    }

    void FooFunction(int x, int y, FooDefaults defaults)
    {
        ...
    }

    void usage_code(...)
    {
        int x = 1;
        int y = 2;
        FooDefaults defaults;
        defaults.arg2 = 9;
        FooFunction(defaults);
    }
2 comments

> you can't pass the default values around, they can't flow in the code.

I see you worked very hard on those contortions just to find some way to call them dataflow ;)

The values can obviously be passed around just fine. The issue is duplication of their source of truth, not their inability to be passed around. And the duplication of the source is easy enough to fix - if you don't want to hard-code them then you can just make a static function (or constant) that returns them so callers can refer to that same value without duplicating the source of truth. No need to throw entire the baby out with the bathwater.

(And the struct solution is an alternative to unnamed arguments, not to default arguments per se. It has its own advantages and disadvantages.)

To be more precise you can't read default values from the source of truth, which is the parameter list. There is syntactically no way, and they aren't materialised in any accessible way at compile time.

> you can just make a static function (or constant) that returns them so callers can refer to that same value without duplicating the source of truth

Oh, but now you have duplicated it. Maybe not a value literal, but you still have to synchronize the default value expression (constant reference or whatever) between the parameter list and every other place that is interested.

And at any place that is not interested in such data you still have to meticulously forward the precise list of default values in the right order to a final consumer of the data.

You cannot

    void func(int x = 1, int y = 2, int z = 3);

    void usage()
    {
        int x = gimme_default(func, x);
        int y = gimme_default(func, y);
        int z = gimme_default(func, z);
        ...
    }
You also cannot

    void func(42, PASSDEFAULT, -1);
As you explained you can

    constexpr int FUNC_DEFAULT_X = 1;
    constexpr int FUNC_DEFAULT_Y = 2;
    constexpr int FUNC_DEFAULT_Z = 3;

    void func(int x = FUNC_DEFAULT_X, int y = FUNC_DEFAULT_Y, int z = FUNC_DEFAULT_Z);

    {
        int x = FUNC_DEFAULT_X;
        int y = FUNC_DEFAULT_Y;
        int z = FUNC_DEFAULT_Z;
        ...
    }
but that's already too painful for me to write, when the more realistic setting is that there is also

    void func_variant1(int foo, int x = FUNC_DEFAULT_X, int y = FUNC_DEFAULT_Y)
    {
         do_foo(foo, x, FUNC_DEFAULT_Y);
         func(x, y, FUNC_DEFAULT_Z);
    }

    void func_variant2(int foo, int bar, int x = FUNC_DEFAULT_X, int y = FUNC_DEFAULT_Y, int z = FUNC_DEFAULT_Z); // etc

There is already significant boilerplate / repetition and I have a sense that there will be a new FUNC_DEFAULT_W coming soon that has to be inserted in 53 places. This is the textbook application for structs, which can abstract over sets of primitive data items.

Code that makes significant use of default arguments always has that unstable, arbitrary sense to it, with function parameter lists that grow too long, and it always seems to be badly structured and fragile. I cannot prove it formally but I have the hair to prove that I've spent months of my life refactoring such code.

   void FooFunction(int x, int y, optional<int> optarg1 = {}, optional<int> optarg2 = {}) {
     int arg1 = optarg1.value_or(3);
     int arg2 = optarg1.value_or(7);
   }

   void usage_code() {
     FooFunction(x, y, {}, 9);
   }
I.e. for complex interfaces defaulted arguments should default to an out-of-band placeholder, not to the actual value.

I do like the struct as well, but it is still not ideal if you want to use initializers. I.e this doesn't work in C++:

   FooFunction(x, y, {.arg2 = 9});
You have to specify all preceding values FooFunction(x, y, {.arg1 = 2, .arg2 = 9});

Works better with optional (and converting everything to a struct):

   FooFunction({.x = x, .y=x, .arg1 = nullopt, .arg2 = 9});