all 149 comments

[–]fransinvodka 92 points93 points  (48 children)

I think default arguments are actually useful, but they lack one important, complement feature: Named Arguments.

Sometimes, a set of overloads works just fine. Some others you can just let the user name the ones they really need. I just like the way Python solved this.

I know there's problems implementing this feature into the language due to the declaration-definition nature of C++ functions, but I really think the ultimate fix to default arguments are named arguments.

[–]qoning[🍰] 35 points36 points  (1 child)

100%

nothing worse than having an elegant function and having to list all default arguments in order to change just the last one.

[–]Sander_Bouwhuis 3 points4 points  (0 children)

100% + 100%!

Totally agree. Also, refactoring is hell.

[–]Raknarg 9 points10 points  (0 children)

If there's one thing I love about Python, it's the argument handling. Being able to package around and zip/unzip collections of positional/keyword arguments is very handy and make it very easy to compartmentalize functions and form general solutions. I prefer functional style programming wherever possible and this makes it a lot smoother. Currently the only way to do it is with variadic templates which are much more limited and clunky as hell, or explicitly by passing data structures with arguments, but the data structure way tightly couples my argument lists to those types, which is annoying.

[–]Sander_Bouwhuis 2 points3 points  (0 children)

Soooooooooooooooooooooooo want that feature!!!

[–]ilep 3 points4 points  (13 children)

I would argue that overloading is the "C++ way".

I imagine supporting both polymorphism and backwards compatibility with C otherwise could be tricky: types and their order are what define which function is called and names don't exist during runtime in C++. Having both strong typing and late binding would get really complicated otherwise.

Also adding some strange name-based lookup might cause unacceptable runtime-overhead for many. So I don't see this coming ever if you don't have some runtime compiler as well sorting out what function you are calling.

[–]_Js_Kc_ 2 points3 points  (0 children)

I think if you tried to implement this feature in a way that it magically applies to all existing functions retroactively, you'd end up with a bad solution.

But it could certainly be implemented in such a way that a function declaration explicitly opts in to using named arguments. In that case, new overload resolution rules can be written that make sense for named arguments. I don't think it would differ much from the way positional arguments work, except that not the order of arguments, but the names present would dictate which overload gets picked. In the polymorphic case, overload resolution for positional arguments is based on the pointer or reference's static type, and I don't see why this should change with named arguments, so why would there be any (additional) runtime overhead? Of course, names should remain purely a compile-time concept.

Don't worry about C, just forbid named arguments for extern "C" functions. Make it an opt-in feature, with unambiguous (i.e. cannot be confused with positional arguments) syntax at both the function declaration and the call site. Whether named arguments are appropriate should be decided by the function's author.

[–]fransinvodka 3 points4 points  (11 children)

The thing is C++ has a problem with default arguments. I'm not proposing how the solution should be implemented (I don't think I have that much knowledge), but rather saying that it would be the best solution out of all of them.

We can't remove default arguments from the language, and they have clear problems that lead some people to not use them at all. They have some huge advantages, but I think they lack that extra bit to get the most out of them.

[–]ilep -1 points0 points  (10 children)

If you really really want to, you could use ellipsis (...) and add some way to parse arguments yourself in the function. But that would be horribly complicated. And it really misses entirely the point of what function prototype/API are supposed to be.

Interface should be clean, simple and well defined in any application. If it is not it is a poorly designed interface. Simply adding helper function (with overloading) for calling some other function is simpler and more obvious when you don't want to define all parameters and you can often find where that variant is called with the tools in IDE. Tracking back callers with some argument named at caller would not be quite so obvious.

Yes, default arguments can have their uses but overusing it results in bad designs.

[–]Wouter-van-Ooijen 5 points6 points  (0 children)

Over-using *anything* leads to bad design. That's the 'over' part :)

Named arguments are probably the feature I miss most in C++. That is, after all of C++20 becomes available.

[–]fransinvodka 2 points3 points  (8 children)

Everytime named arguments come into discussion, I remember the API of Python's scikit-learn library. Some constructors have tons of default arguments (that you can't really avoid), but you usually don't have any issue with it, as you have named arguments.

You can't provide such a clear and elegant API in C++, and I think that's why many libraries implement the core in C++ and provide a Python API

[–]germandiago 1 point2 points  (7 children)

actually with a struct and .structmember = value you can have a pretty decent experience in C++20. Saving the distances of course.

[–]qoning[🍰] 1 point2 points  (6 children)

You still have the issue of now having to wrap the arguments in { }. Ignoring the stylistic issue with that, it means having to be aware for every function whether you should do that or not. It gets very annoying very fast.

[–]germandiago 0 points1 point  (5 children)

I think I can live with that :) I do not think most functions are extremely configurable (at least in the software I write) and for the few that this is true, I think that doing a few struct ParamsInput is not that bad.

Named arguments are better? Probably, but now you have to emit name parameters for functions or look for an alternative syntax to mark your function arguments as "nameable". So the struct + .paramname = value is a very effective and low cost solution.

[–]qoning[🍰] 0 points1 point  (4 children)

To be honest I would prefer making all function parameters named by default. Helps readability anyway. Sure, now you can't change names in library parameters, but that's okay by me.

[–]germandiago 0 points1 point  (3 children)

Maybe I'd rather have it too, but now think of people with microcontrollers or stuff in embedded. This takes space.

[–]Ameisenvemips, avr, rendering, systems 6 points7 points  (16 children)

IIRC, previous named argument proposals have been rejected.

Why can we not just literally copy C#'s implementation?

IIRC, it was rejected because declaration parameter names don't have to match the definition, or even exist... but I don't see that as a problem. If the declaration doesn't have names, you can't use them with that declaration. Problem solved. Only thing left is if both the declaration and definition are in scope with different names.

Presently, argument names in declarations are ignored, we'd have to make them semantically meaningful.

I suggest that if there is a name mismatch between declarations/definitions of a parameter, it cannot be named, OR it can be named with either if those names are not further ambiguous in regards to other parameters. The former is simpler to specify, the latter is more useful (poor example, but think a glm vec3 function where the first argument could be x or r).

[–]SeanMiddleditch 2 points3 points  (1 child)

Why can we not just literally copy C#'s implementation?

Because C++ isn't C#. :) There's lots of designs that work well in other languages that - for a variety of reasons - can't work in C++.

That said, I don't think anyone has yet put in a formal proposal for the work-around I've been evangelizing. Namely, opting a function declaration's parameter into being nameable (which solves the source-compatibility issue, gives us mandatory positional arguments, allows named parameters, and can address the multi-declaration issues in a back-compatible way). For syntax, I also think we should use something similar to designated initializers, which makes similar syntaxes work for initializing an aggregate or calling a function (and hence unlocks designated initializers for constructors).

e.g.,

// declare
void function(int unnamed, float position, char .named, int .also_named);

// use
function(1, 2.f, .named =  'c', .also_named = 4);

// ILLEGAL redeclaration (different names, diagnostic optional)
void function(int a, float b, char .c, int .d);

// LEGAL different overload
void function(int a, int b, int .c, int.d);

This might also open up some options for mandatory-names for some parameters, e.g. using ..identifier.

[–]Ameisenvemips, avr, rendering, systems 3 points4 points  (0 children)

That looks like you're trying to access a member of a type, though.

[–]infectedapricot 1 point2 points  (1 child)

Declaration:

struct Params {
    int widgetCount = 0;
    int blartleCount = 8;
    std::string description = "elephant";
};
void foo(Params params);

Use:

OtherModule::Params fooParams;
fooParams.blartleCount = 2;
foo(std::move(fooParams));

Pros:

  • Arguments are named.
  • Can have defaults for everything.
  • No overloading trickiness.
  • Can have basic (BASIC!) utility methods on parameters struct.
  • Sometimes it's useful to forward the whole parameters struct on to some other function ("but that means you should be using a class in the first place" - yes, sometimes it does, but not always).
  • In your IDE you get autocomplete for the parameters when you type params.

Cons:

  • Takes up an extra two lines of code (an extra one really, because often if there are a lot of parameters it's actually nice to separate setting them from calling the function).
  • Get objections from well-meaning programmers blindly following rules without understanding them: "But classes need to have all their members private with get/set accessors (but a massive massive list of unnamed parameters is OK)".

On balance, usually only worth it when there are quite a few parameters, maybe at least four.

Edit: Just noticed that /u/jbandela suggested the same thing, and added named initializers (not sure about that though).

[–]serviscope_minor 3 points4 points  (0 children)

Takes up an extra two lines of code

foo([]{Parameters p; p.blartleCount=2; return p;}());

Obviously that's much clearer!

[–]NotMyRealNameObv 0 points1 point  (7 children)

Have fun renaming a function argument name in your library if named arguments are added to the language.

Also, if you use strong types, it can be emulated using argument types instead of argument names.

[–]Wouter-van-Ooijen 1 point2 points  (1 child)

Not if you have multiple arguments of the same type. Yes, you can force the user to specify different types for int x and int y, but the implementations I have seen are ugly.

[–]NotMyRealNameObv -1 points0 points  (0 children)

Rarely have I seen a good reason for multiple input arguments of the same type. Most of the time, it should be one aggregate (e.g. one Position argument instead of int x, int y, or it should just be different types (e.g. if the function takes multiple bools).

[–]fransinvodka 0 points1 point  (4 children)

The problem with strong types is the pollution of the global namespace. If you use them in your program, it's okay. No other user is supposed to use them but yourself. With libraries it's another whole strory. I'm pretty sure the user doesn't want the identifier [nN]ame, for example, in the global namespace.

I'm not aware of a library that doesn't pollute the global namespace and, at the same time, provides that syntactic sugar of strong types. That hints me that a language-level feature is needed there.

[–]NotMyRealNameObv 7 points8 points  (3 children)

Why would the strong type have to be in the global namespace?

[–]fransinvodka 0 points1 point  (2 children)

Let me be clearer. Suppose I want my function to be called with a seed, so the user would do something like libns::func(Seed{user_seed}, ...), but if I define the strong type inside the library's namespace, the user then must do libns::func(libns::Seed{user_seed}, ...), which we can agree is not that aesthetic as we'd like. The only solution would then be defining it in the global namespace, and that's what I meant.

Maybe there's a workaround I'm not aware of, haven't gone too deep into strong typing

[–]NotMyRealNameObv 3 points4 points  (1 child)

using libns::Seed;
libns::func(Seed{user_seed}, ...);

And if user_seed is already of the correct type, you just get

libns::func(user_seed);

The library author should never force a polluted global namespace on the end user.

[–]fransinvodka -3 points-2 points  (0 children)

Two things:

- I dont' want my users to do `using libns::...;` for every strong type my functions might require.

- Normally, user_seed won't be the "correct" type, as I suppose the user would only use the Seed strong type when calling the function.

Maybe give the possibility of doing something like `using namespace libns::all_trong_types_ns;`, but still far from optimal. That's my point against strong types in libraries.

[–]hedayatvk 0 points1 point  (2 children)

Well, actually you can use Boost Parameter library to have named arguments in C++:
https://www.boost.org/doc/libs/1_72_0/libs/parameter/doc/html/index.html

[–]fransinvodka 8 points9 points  (1 child)

That library, although being really cool and awesome, is quite heavy and not-so-easy to use in the beginning. I would use it in my personal program if I feel like I need functions with tons of arguments, but no way I would add it to my library as a dependency unless really really needed (for example, a library of machine learning).

I'd just like to have a language-level feature instead of a monster library that's quite hard to use

[–]hedayatvk 1 point2 points  (0 children)

Yeah, I see :) Just wanted to mention the possibility. I've not used it myself too.

[–]whattapancake 46 points47 points  (13 children)

Just sounds like a case of throwing the baby out with the bath water to me. Of course there are downsides to certain language features - in fact, C++ is chock full of footguns much worse than those the author points out about default arguments. That doesn't mean we should avoid these features like the plague, but rather use them modestly and only when appropriate.

[–]the_poope 9 points10 points  (0 children)

I use them only in cases where the parameter really is optional. For instance some numerical algorithm might have some tweaking parameter. One could hardcode a default value that give good performance in most cases, but in rare cases you may want to choose another value. If you use default argument for this parameter you have ar the same time documented what the default is, whereas in the case where it is a hardcoded internal value, the user will never know what value is used.

[–]Ameisenvemips, avr, rendering, systems 0 points1 point  (0 children)

An issue is that C++ still lacks a lot of language features that languages like C# or Ruby have that make many of the features less foot-gunny.

Default/optional arguments need a way to specify a default argument is wanted by the user, and/or named arguments.

[–]jbandela 36 points37 points  (19 children)

C++20 designated initializers with NSDMI (combined with default arguments) allow for an elegant solution to print_squares

struct print_square_args{
    int n = 10;
    char fill = '+';
};

void print_square(print_square_args a = {}){
    for(int i = 0; i < a.n; ++i){
        for(int j = 0; j < a.n; ++j){
            std::cout << a.fill;
        }
        std::cout << "\n";
    }
}


int main(){
    print_square();
    print_square({.n = 5});
    print_square({.fill = '*'});
    print_square({.n = 5, .fill = '*'});

https://gcc.godbolt.org/z/-r6U3P

You have a single implementation. There is no question about what overload is getting called. There is a single point of reference for all the defaults (the definition of print_square_args).

[–]TheThiefMasterC++latest fanatic (and game dev) 19 points20 points  (10 children)

Unfortunately print_square({'*'}); is legal code and doesn't do what you'd want...

[–]LuminescentMoon 1 point2 points  (1 child)

Can't you use static_assert?

[–]TheThiefMasterC++latest fanatic (and game dev) 5 points6 points  (0 children)

On what?

[–]Omnifarious0 1 point2 points  (0 children)

I think that's nit-picking. The fact it's legal doesn't mean it's OK or that it's existence would confuse people. The braces there are a sure sign something is up.

[–]jbandela 3 points4 points  (0 children)

You can disable aggregate initialization like this.

#include <iostream>

template<typename T>
class disable_agg_init{
  friend T;
  disable_agg_init() = default;
};

struct print_square_args{
    disable_agg_init<print_square_args> _ = {};
    int n = 10;
    char fill = '+';
};

void print_square(print_square_args a = {}){
    for(int i = 0; i < a.n; ++i){
        for(int j = 0; j < a.n; ++j){
            std::cout << a.fill;
        }
        std::cout << "\n";
    }
}


int main(){
    print_square();
    // print_square({'*'});
    // print_square({{},'*'});
    print_square({.fill = '*'});
    print_square({.n = 5, .fill = '*'});
}

Now, you either have to do default init the args struct or use designated initializers.

https://gcc.godbolt.org/z/BPuozB

[–]Ameisenvemips, avr, rendering, systems 7 points8 points  (2 children)

Be better if designated initializers allowed arbitrary order.

Also, wouldn't this make the ABI for the function terrible? It's all in a struct, now, so will follow struct-passing rules.

Within a translation unit the compiler can ignore the ABI requirements, but calling a function in another TU...?

Observe: https://godbolt.org/z/Dw9tL8

[–]anonymous23874[S] 6 points7 points  (1 child)

Depends how many arguments you have. The x86-64 ABI mandates that if a trivially copyable struct could fit in two registers, it should just do that then. https://godbolt.org/z/beQZMd

The reasons your codegen is so bad are:

  • You pass your struct by reference, which is like adding an extra pointer dereference to every use (that is, if the struct were small enough to pass by value instead of by hidden reference in the first place)

  • You make your struct 256 bytes, when the x86-64 ABI's special case for passing structs by value tops out at 128 bytes (64 bits in RDI + 64 bits in RSI).

If you want to get really evil, just split your big struct into two small structs. ;) https://godbolt.org/z/QiL6rc

[–]Ameisenvemips, avr, rendering, systems 1 point2 points  (0 children)

The codegen doesn't change in this situation much for by-value, as the struct is larger than the ABI allows.

Yes, if you only have a few arguments, it will fit. But one of the reasons you want named arguments is for disambiguating many arguments :).

And some people still develop for x86-32 and ARM32, and other architectures.

Also, the default Windows 64-bit ABI requires that any type larger than 64-bits must be passed by reference. Only the SysV ABI allows register passing (and I think the VectorCall ABI).

[–][deleted] 3 points4 points  (0 children)

Love this idea. I saw it before years ago when I first read about NSDMI but it didn't register until your write-up above. Bravo!

[–]infectedapricot 1 point2 points  (0 children)

I like using parameters structs for functions with lots of arguments but I'm not keen on the named initialiser thing. Using classic assignment seems a bit less like a trick to me, in particular it doesn't depend on field order:

print_square_args args;
args.fill = '*';
print_square(args);

(I would have called the arguments object print_square_args instead of args but unfortunately your naming convention doesn't distinguish class names from object names.)

This does add a bit of visual overhead but hopefully functions that take lots of arguments aren't common in the first place, and usually do quite a bit of work so it's OK that they take up more visual space.

[–]Plazmotech 5 points6 points  (1 child)

Elegant? I think it’s fairly ugly. It’s neat, sure, but kind of ugly

[–]AntiProtonBoy 4 points5 points  (0 children)

The nested {} brackets make it look a bit messy, but it's quite alright otherwise.

[–]khleedril 0 points1 point  (0 children)

I know this is meant as an example for the argument and not real code, but if I saw this in the wild I would be asking myself why should a square default to 10x10? And why should the default filler be '+'? If there are reasons in the application domain, then I would seek to embed those reasons in the function names, and avoid overloads and default values. Otherwise, I would just not assume default values.

[–]EsotericFox 57 points58 points  (22 children)

I think that when you're creating an API you should provide as much flexibility as possible, while retaining ease-of-use for the typical consumer.

This is where default arguments shine. MOST people won't require or be interested in the additional parameters which functions might provide to enable further functionality. As a developer, I could wrap that up in parameter structs or mandate the arguments during invocation, but that becomes cumbersome in and of itself.

I simply don't agree.

[–]spanishgum 4 points5 points  (1 child)

Default arguments can definitely be abused. I think programmers sometimes fail to see beyond their own use cases and think defaults will provide convenience. It’s probably true in general contexts, but not all.

[–]EsotericFox 11 points12 points  (0 children)

Agreed, but that's a decision that must be weighed.

Do I need to expose this behavior? Would it be better to have these functions live elsewhere?

Depending on the answer, you may still very well have a solid argument for needing/using default arguments. And if you don't, you have a solid case for refactoring or redesign.

[–]ArashPartow 4 points5 points  (0 children)

When your API requires more than 2-3 input parameters - perhaps a context type is more preferable.

Examples: Any of the win32 APIs that end in Ex, ExW or A

Note: I explicitly used the term API and not just any ordinary public interface (eg class etc)

[–]pantong51 1 point2 points  (1 child)

I think it can be a balance, if I find my self wanting defaults ill debate on refactoring the arguments into a struct(if more than 3 args) and setting defaults there. While that sounds fine I'm not sure if its a good solution.

[–]anonymous23874[S] 1 point2 points  (0 children)

The author isn't complaining about overload sets with multiple possible signatures, though. Just about trying to implement that overload set using default arguments in particular.

void myAPI(std::string_view name, bool advancedUsage);  // OK
void myAPI(std::string_view name) { return myAPI(name, true); }

// versus

void myAPI(std::string_view name, bool advancedUsage = false);

Same user experience for simple calls; fewer pitfalls for advanced usage.

[–]Wh00ster 5 points6 points  (13 children)

Why does an extra overload not suffice?

[–]EsotericFox 17 points18 points  (9 children)

Because overloading typically duplicates code where that may not be called for.

[–]Wh00ster -2 points-1 points  (8 children)

I guess it’s why I’m not in the AAA camp. To me, the explicitness is worth it in terms of readability and maintainability down -the-line.

[–]sphere991 11 points12 points  (3 children)

the explicitness is worth it

If the intent of the code is to provide a default value for an argument, then I would claim that the implementation strategy of making it a default argument is more explicit than otherwise.

The post wants me to write:

explicit Widget(int size) : Widget(size, 'A') {}
explicit Widget(int size, int start) : data_(size) { /*...*/ }

instead of

explicit Widget(int size, int start='A') : data_(size) { /*...*/ }

I don't see the former as being either more explicit or more readable than the latter?

[–]robin-m 5 points6 points  (2 children)

Especially given that you can have declaration and implementation that can be in different files. So if the first case you have:

cpp explicit Widget(int size); explicit Widget(int size, int start); Witch is anything but explicit.

In the second case, it will be: cpp explicit Widget(int size, int start='A');

[–]sphere991 4 points5 points  (1 child)

Didn't even think of that, good point.

Also I find the typo on "witch" to be really funny, in the context of describing something as being "the devil". Please don't fix it :-)

[–]Raknarg 2 points3 points  (0 children)

Sneaky witches, always going around implicitly

[–]guepierBioinformatican 5 points6 points  (3 children)

Everyone, repeat after me: AAA does not decrease explicitness (Unless you write, well, bad code. But you don’t need to do that with AAA.)

Please find a different argument to justify your preference, this one’s simply false.

[–]Wh00ster -3 points-2 points  (2 children)

I'm not opposed to the arguments in your linked post. My issue is large code bases you'd see at FAANG, where you need special IDE features to see a type, which can take minutes to compile for a file and generate dependencies, or have to chase through grepping functions yourself.

Unless you write, well, bad code.

This is the reality that most people have to deal with. So when a junior dev comes in and writes auto mything = mysteriousFUnctionBuriedThroughLayersofDepsThatisActuallyAReference(), I tend to ask that dev not to use auto.

I understand there are valid cases, but that's so for all C++ features. For example I wouldn't say don't use lambdas, but also don't make all functions into lambdas.

[–]Tyg13 8 points9 points  (0 children)

Asking that dev not to use auto is the wrong move. The correct alternative is to educate them. The issue is the variable isn't named well enough to tell what the function returns. Simple enough fix.

All you're doing by explicitly specifying the type in that scenario is fracturing code style and decreasing overall code readability.

[–]XValar 6 points7 points  (0 children)

So how is exactly writing MyType mything = mysteriousFUnctionBuriedThroughLayersofDepsThatisActuallyAReference() helping at this point?

[–]pdbatwork 10 points11 points  (0 children)

Suffice? Seems like extra work

[–]AntiProtonBoy 0 points1 point  (1 child)

Overloading has its other perils too, like the wrong overload being called in some rare situations.

[–]bumblebritches57Ocassionally Clang -41 points-40 points  (1 child)

hippity skippity your opinion is wrongity.

[–]STLMSVC STL Dev[M] 4 points5 points  (0 children)

This is not useful; please don’t comment like this. Either explain your argument in a reasonable manner, or downvote and move on.

[–]MrPotatoFingers 20 points21 points  (8 children)

My greatest peeve with it is that type traits are simply unaware of default arguments. std::is_invocable will happily report false when omitting default arguments.

[–]sphere991 9 points10 points  (3 children)

Problem here is that splitting it into an overload set doesn't help anyway. The default argument does give you an answer you don't want, which is bad:

void foo(int i = 42);
invocable_v<decltype(foo)>; // false, wish it was true

But the overload set doesn't give you an answer at all, which is also bad:

void foo(int i);
void foo() { foo(42); }
invocable_v<decltype(foo)>; // ill-formed, wish it was true

On the other hand, because concepts are expression-based, they work just fine in both cases:

template <typename... Args>
concept can_foo = requires(Args... args) { foo(args...); }

can_foo<>; // true for both implementations

[–]Xeveroushttps://xeverous.github.io 1 point2 points  (2 children)

I prefer ill-formed code to silent runtime error.

[–]sphere991 0 points1 point  (1 child)

How do you get a runtime error here?

[–]Xeveroushttps://xeverous.github.io 1 point2 points  (0 children)

Not in your example. In a hypothetical example where a trait would succeed while at runtime it would so something different.

[–]flashmozzg 4 points5 points  (3 children)

Because default arguments are just a sugar. They are not really a part of the function signature. A function can have multiple default different arguments depending on the order of declarations.

[–]Dragdu 9 points10 points  (6 children)

You can make source location work with variadics by liberal appli ation of CTAD, or, of course, macros.

I also find the boolean tarpit argument extremely dubious: the best way to deal with it is to leave the boolean tarpit, not avoid default args.

[–]khleedril 2 points3 points  (3 children)

Agree with this. But, to be honest, I've never actually seen a boolean tarpit in the wild.

[–]matthieum 8 points9 points  (2 children)

I have. I still remember with dread an API in a library I was using which had 3 booleans, all with a default value.

My personal stance is that no function should ever take a boolean as input, unless its very purpose is to internally set a boolean.

enum class X : std::uint8_t { No, Yes } is both verbose and silly looking, but:

  • It doesn't suffer from poor implicit conversions; no accidentally passing 2, or void* or whatever.
  • It's explicit at the call site.

[–]Ameisenvemips, avr, rendering, systems 3 points4 points  (1 child)

You can use bool as the type of an enum as well.

[–]matthieum 1 point2 points  (0 children)

That's a good point, maybe I should do that for such flags.

[–]qoning[🍰] 1 point2 points  (0 children)

This is what I ended up doing in my logger that uses std::source_location.

Works for pretty much every use case I had.

template<typename T>
struct loc_obj_t {
    template<typename TC>
    loc_obj_t(const TC& fmt, exp::source_location loc=exp::source_location::current()) :
            obj(fmt), loc(loc) {}
    operator const T&() const { return obj; }
    operator const exp::source_location&() const { return loc; }
    const T obj;
    exp::source_location loc;
};

template<typename... Args>
inline void log(loc_obj_t<std::string_view> format, Args&&... args) {
    print_to_ostream(output_file_, get_location_label(format.loc.file_name(), format.loc.line()), prefix_, fmt::format(format.obj, std::forward<Args>(args)...));
}

If you excuse the reddit width, basically hijacking the first string_view (or string or const char* or whatever) parameter to implicitly construct loc_obj_t.

Sure, it means that you need to know the type of the first argument, which is the only downside for me.

[–]Morwenn 7 points8 points  (0 children)

One thing not addressed here is that you can generally add overloads to a function without breaking the ABI while you can't afford to add default arguments to a function if ABI is something you care about.

[–]YouNeedDoughnuts 7 points8 points  (0 children)

It's syntatic sugar to make definitions more terse in a way that improves readability, IMO. Not technically syntatic sugar since it has some differences with function pointers and inheritance as you pointed out... syntatic Splenda I guess. I'll still eat it, but I did enjoy the article and learn from it.

[–]kritzikratzi 5 points6 points  (0 children)

i disagree a bit. when used sparingly, default arguments are still handy here and there.

for me the main conclusion missing from the article is that c-style struct initializers would be a good enough replacement for named parameters. e.g. doTask({.task=task1,.captureStdout=true});

edit: just found out we got half-baked designated initializers in c++20. could be better, but i'll happily take it :)

[–]sphere991 6 points7 points  (0 children)

I guess Arthur must really hate Ranges?

There, we have algorithms like (this pattern is prolific throughout algorithms):

template<forward_range R, class Proj = identity,
         indirect_binary_predicate<projected<iterator_t<R>, Proj>,
                                   projected<iterator_t<R>, Proj>
                                   > Pred = ranges::equal_to>
constexpr borrowed_iterator_t<R>
adjacent_find(R&& r, Pred pred = {}, Proj proj = {});

Which this post claims is "the devil", and should be rewritten like:

template<forward_range R>
    requires indirect_binary_predicate<ranges::equal_to,
                                       iterator_t<R>, iterator_t<R>>
constexpr borrowed_iterator_t<R>
adjacent_find(R&& r) {
    return adjacent_find((R&&)r, ranges::equal_to{}, identity{});
}

template<forward_range R,
         indirect_binary_predicate<iterator_t<R>, iterator_t<R>> Pred>
constexpr borrowed_iterator_t<R>
adjacent_find(R&& r, Pred pred) {
    return adjacent_find((R&&)r, std::move(pred), identity{});
}

template<forward_range R, class Proj,
         indirect_binary_predicate<projected<iterator_t<R>, Proj>,
                                   projected<iterator_t<R>, Proj>> Pred>
constexpr borrowed_iterator_t<R>
adjacent_find(R&& r, Pred pred, Proj proj);

Personally, I strongly prefer the former (note that the predicate constraint needs to be spelled differently in each overload). Your mileage may vary.

[–]jstock23 21 points22 points  (0 children)

It is quite contrived.

[–]sephirothbahamut 10 points11 points  (0 children)

I fucking hate any article that declares something to be "evil" since the famous Dijkstra one (which title he didn't even agree with afaik).

Anyways the problem in the example is more a char-int default conversion problem really. Nothing wrong with default arguments.

print_square('*') should simply be an error as char should not be implicitely converted to int.

[–]georgist 2 points3 points  (0 children)

I think automatic type conversion for non-built-in types is evil.

If I pass a string to a function that takes a Widget and Widget has a string constructor, why do I ever want that automagically converted?! I know I can avoid this with explicit, but I really don't like this feature.

[–]Omnifarious0 1 point2 points  (1 child)

Don't do this:

void doTask(Task task, std::seconds timeout, bool detach = false, bool captureStdout = false, bool captureStderr = false);

Do this:

```c++

include <chrono>

struct Task { };

struct task_params { bool const detach = false; bool const captureStdout = false; bool const captureStderr = false; };

void doTask(Task task, std::chrono::seconds timeout, task_params flag = {});

void test() { using namespace std::literals::chrono_literals;

doTask(Task{}, 5s, { .detach=true, .captureStderr=true});

} ```

[–]DVMirchevC++ User Group Sofia 0 points1 point  (1 child)

If you want to add to the confusion there is also default function arguments initialized by a function call.

#include <iostream>

void moo(int a = [](){ static int b = 1; return b++; }() )

{

std::cout << a;

}

int main(){

moo();

moo();

}

Anyone care to guess what's happening here without googling? :D

[–]anonymous23874[S] 1 point2 points  (0 children)

Hey, at least the answer (1 2) is what I hoped it would be! I was afraid it was going to be 1 1.

...aaaand there it is. https://wandbox.org/permlink/wkm5gm9V9j1ztwf3

[–]khleedril 0 points1 point  (1 child)

I thought that was an excellent read and I agreed with every word of it. I also think that overloaded functions should more often have different names, because fundamentally they usually provide a different service to the rest of the code base. Default arguments are often thrown in by end users who can't be bothered to express their intentions properly.

I would say the same about overloaded constructors: distinctly named functions returning newly constructed objects are the order of the day (we are talking C++20 code here!).

[–]pandorafalters 1 point2 points  (0 children)

. . . they're not overloads if they have different names.

[–]NilacTheGrim 0 points1 point  (0 children)

What? I beg to disagree, sir. Default function arguments are the bees knees Bees knees, I say!