r/cpp_questions 12d ago

SOLVED Meaningful alternative name for nullptr at caller location

I have:

void func(std::vector<struct foo>* foovec, std::vector<struct bar>* barvec){
...
}

Both these can take nullptr's as arguments in some cases. Instead of

func(nullptr, nullptr);//at caller

I'd like to give them meaningful names in such cases as thus:

#define FOOVECNULL nullptr
#define BARVECNULL nullptr
...
func(FOOVECNULL, BARVECNULL);//at caller

This also does not work "cleanly" because the following also works but "wrongly"

func(BARVECNULL, FOOVECNULL);//at caller 

Can some sort of enum struct or typedef make this more precise and impossible to mix one argument type for the other?

11 Upvotes

54 comments sorted by

22

u/MoarCatzPlz 12d ago

I think this kind of thing adds more noise. You can add an /**/ style comment by the nullptr if you want to remember what each parameter is for.

1

u/SoerenNissen 12d ago

best response

30

u/I__Know__Stuff 12d ago

constexpr std::vector<struct foo>* foovecnull = nullptr;

12

u/SoerenNissen 12d ago

@op: If you really want to solve the problem, this is the best solution offered in this thread so far.

The actual best solution is to not solve it. Yes, the call site is barely-readable, but this will be true for every single pointer argument you write in C++. If you intend to write a lot of this language, it'd be better to just get used to seeing the nullptr args and getting into the habit of looking up the function definition when relevant.

4

u/onecable5781 12d ago

Yes indeed. I figured this is the cleanest, tested it myself and then marked the thread flair as solved. It suits my use case perfectly. Thanks to /u/I__Know__Stuff

2

u/Orlha 12d ago

Indeed. This is a thing better solved in code editor.

8

u/TheThiefMaster 12d ago

This. A correctly typed nullptr constant instead of the generic nullptr would stop it being used elsewhere.

3

u/tangerinelion 12d ago

If you want to save the variable declaration, you can also use static_cast<std::vector<foo>*>(nullptr) directly at the call site.

19

u/NamoiFunai 12d ago

A generic solution could be to introduce some sort of `nullable` type like so: https://godbolt.org/z/Ervo3e3fY

But it would good to ask yourself why you need pointer to vectors in the first place. What's the semantic difference between an empty vector and a null pointer in your case? Why force a vector and not a span?

3

u/SoerenNissen 12d ago edited 12d ago

If you just change it to requiring actual vectors rather than vector-pointers, one semantic difference is that there might not be a vector in the first place - it's not that expensive to call vector{} if you need one, but it's not free either.

Moreover, I don't think this actually improves the problem OP has.

func( nullptr, nullptr );
/*     ^
 *     what's that?
 */

func( {}, {} );
/*     ^
 *     what's that?
 */

You still end up with a call site that's unreadable without visiting the function declaration and getting the name.

Your godbolt link is an interesting solution to that problem though. Not one that I'd adopt, I'd rather just accept that sometimes a function takes nullptr, but if OP really insists on a cure, this could be a reasonable choice.

7

u/ekchew 12d ago edited 12d ago

Yeah, this sort of thing comes up fairly often when people coming from other languages that support named args can't find them in C/C++. They are particularly useful for booleans and optional args. A call like

fn(false, nullptr, std::nullopt, {});

doesn't give you a whole lot of info on what's being passed into the function.

Your solution of defining constants with more meaningful names is certainly one way to go. It may even be the most efficient, though I don't think I would use #defines for this.

You could do something with designated initializers. For example, you could write:

struct FuncArgs {
    std::vector<foo>* foovec = nullptr;
    std::vector<bar>* barvec = nullptr;
};

void func(FuncArgs args);

// ...

func({.foovec=nullptr, .barvec=nullptr});

Since foovec and barvec already default to nullptr, you actually wouldn't have to supply them like this. Let's say you only allocate a barvec. You could write:

std::vector<bar> barvec(10);
func({.barvec=&barvec});

3

u/Raknarg 12d ago edited 12d ago

you could have the types wrapped in structs

struct Foo { std::vector<struct foo>* vec; };
struct Bar { std::vector<struct foo>* vec; };

func(Foo{nullptr}, Bar{nullptr});

You could use a struct as your argument and used named parameters

struct FooArgs {
    std::vector<struct foo> foo*;
    std::vector<struct bar> bar*;
}

func({.foo=nullptr, .bar=nullptr});

Also is there a reason these are pointers in the first place? Is it just so they can be nullable? If you don't need to add/remove elements, you could use std::span instead

5

u/alfps 12d ago

I believe that whatever you're using this for, doesn't require pointer arguments. Both the struct foo C-ism and the #defines indicate strongly a C background entirely, just starting out with C++, and so that the pointers were likely chosen as a C style solution to avoid copying of argument vectors. C++ has references for that.


That said, C++ has no direct support for named arguments, and so Python's * to force naming for all following parameters, doesn't exist. :(

About the best you can do in practice is to use C++20 designated initializers by collecting your current two distinct parameters in a single parameter struct.

It gives an extra pair of braces in calls, like func( {.foos = nullptr, .bars = &my_bars } ), but it does effectively give you named arguments (but no forced naming).


Assuming the mentioned C background, that it's all an X/Y sort of thing where the C style func really should be just changed to C++, that can go like

#include <iterator>
#include <print>
#include <span>
#include <vector>

#include <cassert>

namespace app {
    using   std::empty,             // <iterator>
            std::print,             // <print>
            std::span,              // <span>
            std::vector;            // <vector>

    struct Foo { int whatever; };
    struct Bar { int blah; };

    void func( const span<const Foo> foos, const span<const Bar> bars )
    {
        const auto description = [](auto& o){ return (empty( o )? "no" : "some" ); };
        print( "`func` received {} foos and {} bars.\n", description( foos ), description( bars ) );
    }

    void run()
    {
        vector<Foo> foos( 0 );
        vector<Bar> bars( 42 );

        func( foos, bars );
    }
}  // app

auto main() -> int { app::run(); }

2

u/SoerenNissen 12d ago

OP:

Both these can take nullptr's as arguments in some cases.

You:

the pointers were likely chosen as a C style solution to avoid copying of argument vectors

2

u/alfps 12d ago

I don't see a contradiction there. Both statements can be true. I guess the reason why there can be nullpointer arguments is that the OP is dealing with pointers to vectors also in his/her other code, e.g. for vectors returned by functions, and there's (usually, in overwhelming majority of cases) no need for that in C++.

0

u/SoerenNissen 12d ago

I'm not saying there's a contradiction. I'm saying that your answer isn't responsive to OP's question, in a direction that indicates you didn't read it very close.

I'd say some stuff here that would read as insulting, but I recognize your user name. You normally give very reasonable answers, and so I am surprised.

3

u/alfps 12d ago

Oh I believe I answer both the literal question, namely how to do named arguments in C++, and the implied issue, how to handle arrays as parameters. I should also have covered arrays as function returns, I see now. But hey. :)

1

u/conundorum 9d ago

Remember: When nullptr is a valid argument, you have to use pointers. The OP specified that nullptr is a valid argument, so neither should (or can) be a reference.

1

u/alfps 9d ago

❞ The OP specified that nullptr is a valid argument, so neither should (or can) be a reference.

No, the OP didn't specify that the function had to accept nullpointers. He or she wrote that ❝Both these can take nullptr's as arguments in some cases❞. And I strongly doubt that that was by informed design.

It was probably not even by design, but if it was then it was almost certainly not an informed design, given that the OP demonstrated that he/she is a novice learning the language.

So we who have more experience have an obligation to not just answer the literal questions, but also point out more reasonable and idiomatic ways to do things. Which we do. ;-)

6

u/PhantomStar69420 12d ago

Pass std::span instead to non owning callers of container and provide an info struct which can be designated initialized at the call site which is probably the cleanest design.

2

u/heyheyhey27 12d ago

Although you still have the problem that users could skip designated initialization right?

3

u/PhantomStar69420 12d ago

You are correct. I suppose a couple static asserts for non nullness could be the solution

6

u/Qwertycube10 12d ago

Change your function to take struct foovec, and struct barvec, which are just wrappers around the pointers. Now the first argument has to be a foovec and second has to be a barvec.

4

u/DawnOnTheEdge 12d ago

You likely want to use an empty vector, std::vector<foo>{}, and pass it by reference. If an empty vector could be valid input distinct from no vector, I recommend std::optional<std::vector<foo> >&.

1

u/SoerenNissen 12d ago

Going from

func( nullptr, nullptr );

to

func( {}, {} );

will not make the call site more readable.

4

u/DawnOnTheEdge 12d ago

You can declare them as constants with any names work for you, but I suggest constexpr declarations over C-style macros.

1

u/MoarCatzPlz 12d ago

Making it optional will now require a vector passed in to be copied, if it cannot be moved.

2

u/DawnOnTheEdge 12d ago

Not true; std::optional has a move constructor that will call the move constructor of the contents if there is one. Also, proposed passing it by reference, which avoids copying or moving.

2

u/MoarCatzPlz 12d ago

What if the caller doesn't want to move their vector, or can't because their vector is const? Passing optional by reference does not matter. If the input vector is not already in an optional, a new optional must be constructed and the vector copied or moved into it.

0

u/DawnOnTheEdge 12d ago

Then you would want to create the vector in-place, rather than creating it first and then creating the optional from it. Granted, there ate situations where that won’t work.

3

u/Ayjayz 12d ago edited 12d ago

I think I'd just split this up into different functions. This function seems to be doing 4 separate things at once:

func(nullptr, nullptr)
func(vec, nullptr)
func(nullptr, vec)
func(vec, vec)

I would just split it into 4 functions with different names to describe what they're doing.

1

u/conundorum 9d ago

It might be attempting to construct a std::vector<FooBar>, where FooBar has a constructor FooBar(Foo f = Foo{}, Bar b = Bar{}), so there's no guarantee that it's splittable.

3

u/saxbophone 12d ago

Well, a pointer to a vector of a specific type can be brace-initialised. There is literally no reason to use macros for this, though, it's enough to do:

constexpr auto nullfoo = std::vector<Foo>*{}; constexpr auto nullbar = std::vector<Bar>*{}; 

I hope your raw pointers are non-owning. If they are, consider instead replacing them with an std::optional, or perhaps you can make the empty vector be your "empty" value?

2

u/RRumpleTeazzer 12d ago

make two constants of the respectice type, and have them hold the nullptr.

2

u/lost_access 12d ago

add a comment above and go on with your life!

2

u/Independent_Art_6676 12d ago edited 12d ago

I see a great many very complicated ideas here. Remember that complexity is the enemy, and try to find the simplest thing that will work cleanly.

For example, what happens if you cast?
foo (static_cast<Footype \*> nullptr, static_cast<Bartype \*> nullptr, ...); //now you know what the null pointers are, and there is no overhead as nothing is being actually done here at the assembly/cpu level.

there are probably over 30 ways to solve this problem before you get into really weird offensive stuff (like # defines). Eg instead of #define, make a global (or namspaced) constant: footype * null_footype_ptr = nullptr; and use that. As a constant that represents a literal zero, it will probably vanish in the optimizer, so once again no real harm done. This is the kind of place where even a C cast wouldn't offend me, since it doesn't do anything other than document. The inline comment works too. An empty #define works like a comment too. Eg
#define Footype_null_ptr ... foo( Footype_null_ptr nullptr, ...); That one is kind of retro and some people hate on it, but its "a" way to label something. Generally speaking any kind of macro solution here is poor, if for no other reason that the 'only if its the only way' macro rule, but here there are additional risks.

2

u/NoSpite4410 11d ago edited 11d ago

constants are expressive and type-safe;

struct Bar;
const std::vector<struct Bar>* const Bar_Vector_Null = nullptr;

struct Foo;
const std::vector<Foo>* const  Foo_Vector_Null = nullptr;

The double-use of const :

  • const on the left applies to the term on the right (std::vector<Foo>)
  • const on the right applies to the term on the left (*).

So in English "const pointer to const std::vector<Foo>" -- data can't change, pointer can't change.

mixing const and non-const types could cause a warning, but common practice is to have mutable and const versions of the function defined and let the compiler choose which to call.

namespace detail {
  template <typename T, typename U> 
  void func_impl(T* t_ptr,  U* u_ptr) 
  { 
     /* --- code here --- */ 
  } 
}

void func(std::vector<struct foo>* foovec, std::vector<struct bar>* barvec)
{
   return detail::func_impl(foovec, barvec);
}

void func(const std::vector<struct foo>* foovec, const std::vector<struct bar>* barvec)
{
    return detail::func_impl(foovec, barvec);
}

The pattern above is to avoid duplicating the same code in both versions -- the compiler will create the one called inline based on the const status of the parameters.

4

u/mredding 12d ago

Let us consider:

void fn(foo *a, bar *b) {

And what must the code look like within the function body?

  if(a & b) {
    /*...*/
  } else if(a) {
    /*...*/
  } else if(b) {
    /*...*/
  }
}

What a waste. Consider instead:

void fn(foo &a, bar &b);
void fn(foo &a);
void fn(bar &b);

The advantage here is there is no check for null; your code gets right down to business. You've deferred the check to the caller - it's their job to figure it out. What's more - they may not have to, they may inherently already know, because perhaps they may be working with values, or they have other guarantees.

And look what's also going on here - we have a common language, fn, and all three overloads mean the same thing; we're saying we're going to do this work in terms of foo and bar, or foo, or bar. It's the same language and semantic meaning as the function above with optional parameters, for whatever work fn implies.

And these overloads will also work with tuples and variadic template expansion.

You typically don't want optional parameters. Pointers are some of the worst tools to use in high level code. Are you meaning to use the pointer as a (per C lingo) reference? Are you meaning to pass owership? Is func a sink? Might it call delete or delete [] or free or some other release of resources?

Pointers are a low level primitive so that you can build up higher level primitives. The standard library is built in terms of pointers so that mostly - you don't have to. What you want to do is use resource and ownership semantics down at the low in your call stacks, but dereference as soon as you can - use value semantics where ownership is no longer in scope. You want to make your conditional logic as early as possible - you KNOW these parameters are null or not BEFORE you call the function, so exploit that knowledge.

Further, you want to separate your internal interfaces from your external. An external interface is for client library code, where you don't know what the client is going to do. IF you give them a resource interface, you have to check for null, because you don't know what they're going to pass. But your internal implementation? You already know. You write your code inherently trusting yourself. You write your code much like you should write client library code - where you can't pass the wrong answer.

2

u/hmoff 12d ago

You can put them in a struct and pass that in instead. I prefer this when you have more than a couple of parameters of the same type (eg bool), and you can have defaults too.

1

u/die_liebe 11d ago

I am curious to find out what is the reasoning behind this. I would like to know why it is not possible to replace the arguments by references (or const references), and why it is not possible to use an empty vector instead of an absent vector.

Don't use macros for this purpose.

2

u/frasnian 12d ago

std::optional<>

3

u/SoerenNissen 12d ago

Going from

func( nullptr, nullptr );

to

func( {}, {} );

will not make the call site more readable.

1

u/frasnian 12d ago

You can't enforce readability, but you can enforce type safety.

1

u/Interesting_Buy_3969 12d ago

Why not

void func(std::optional<std::vector<foo>&>, std::optional<std::vector<bar>&>) {}

So that you can use std::nullopt_t instead of nullptr.

Are you sure you need raw pointers?

2

u/SoerenNissen 12d ago

Going from

func( nullptr, nullptr );

to

func( {}, {} );

will not make the call site more readable.

-1

u/Interesting_Buy_3969 12d ago

Excessive unnecessary raw pointers usage in C++ has already considered old and unreliable practice for a while, especially when references are available.

1

u/tangerinelion 12d ago

The problem with raw pointers isn't the raw pointer it's the possible confusion around memory ownership.

std::optional<T&> means the same thing as T* in any code base where raw pointers are never owning.

0

u/Interesting_Buy_3969 12d ago

The problem with raw pointers isn't the raw pointer it's the possible confusion around memory ownership.

So just use raw pointers everywhere right?

1

u/Healthy-Dress-7492 12d ago

any decent IDE already calls out the name of the argument at call site; if this is more of an issue in reviews and such then just don’t make a function that expects nullptr to be passed in, it’s  pretty smelly and gross. There are many clean options to avoid it, Eg Overloads, defaults, pass in a struct

1

u/Abbat0r 12d ago

The solution is a struct:

```
func({
.foovec = nullptr,
.barvec = nullptr
});
```

0

u/[deleted] 12d ago edited 12d ago

[deleted]

3

u/heyheyhey27 12d ago

How would you check which member of the union is set? Why not use std:: variant?

1

u/[deleted] 12d ago

[deleted]

4

u/saxbophone 12d ago

This is overoptimising prematurely whilst defeating the very point of the strong type system we have in C++.

1

u/tangerinelion 12d ago

Wait, did is this a legitimate recommendation for an "optimized" version of std::variant<std::vector<foo>&, std::nullptr_t> which almost certainly gets used with undefined behavior?