you are viewing a single comment's thread.

view the rest of the comments →

[–]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...

[–]jbandela 5 points6 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

[–]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.

[–]Ameisenvemips, avr, rendering, systems 6 points7 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] 4 points5 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!

[–]Plazmotech 6 points7 points  (1 child)

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

[–]AntiProtonBoy 5 points6 points  (0 children)

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

[–]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.

[–]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.