0

In Python I can do something like:

def add_postfix(name: str, postfix: str = None):
  if base is None:
    postfix = some_computation_based_on_name(name)
  return name + postfix

So I have an optional parameter which, if not provided, gets assigned a value. Notice that I don't have a constant default for postfix. It needs to be calculated. (which is why I can't just have a default value).

In C++ I reached for std::optional and tried:

std::string add_postfix(const std::string& name, std::optional<const std::string&> postfix) {
  if (!postfix.has_value()) { postfix.emplace("2") };
  return name + postfix;
}

I'm now aware that this won't work because std::optional<T&> is not a thing in C++. I'm fine with that.

But now what mechanism should I use to achieve the following:

  • Maintain the benefits of const T&: no copy and don't modify the original.
  • Don't have to make some other postfix_ so that I have the optional one and the final one.
  • Don't have to overload.
  • Have multiple of these optional parameters in one function signature.
Alexander Soare
  • 2,825
  • 3
  • 25
  • 53
  • Does [std::optional specialization for reference types](https://stackoverflow.com/questions/26858034/stdoptional-specialization-for-reference-types) help? – Wyck Dec 02 '22 at 14:59
  • @Wyck not sure. There is an answer about using a pointer. But that won't satisfy my desire to make sure the original is not modified. – Alexander Soare Dec 02 '22 at 15:02
  • It feels like the "don't have to overload" requirement here is making things harder than it needs to be. What's wrong with having `add_postfix(const std::string& name)` that calculates the appropriate value for `postfix` and then calls `add_postfix(const std::string&, const std::string&)`? I suppose that scales poorly for your last bullet point. – Nathan Pierson Dec 02 '22 at 15:07
  • @NathanPierson I have two of these parameters in my real use case. Just feels like a lot of overloading. Coming from a Python background I can't help but check if there's a better way. – Alexander Soare Dec 02 '22 at 15:09
  • I don't see how you can avoid modifying the original if you don't want to copy it. Even in your example in python you modify the parameter passed to the function. If you want to replicate exactly that, just go with a pointer and pass it as `nullptr` – Federico Dec 02 '22 at 15:09
  • @Federico nope you don't. In C++ it reads like that, but in Python that line creates a new object and reassigns `postfix` to reference it. In the caller, the original is not modified. So then I think the `nullptr` suggestion won't work because I'd want to protect the original. – Alexander Soare Dec 02 '22 at 15:11
  • @AlexanderSoare When writing C++ code, it isn't the best of ideas to use another programming language as a model in writing the code. You will either end up with 1) Buggy programs, 2) Inefficient programs, or 3) Programs that will look weird to a C++ programmer. It looks like your attempt will fall into category 3. – PaulMcKenzie Dec 02 '22 at 15:16
  • 1
    @AlexanderSoare "[A pointer] won't satisfy my desire to make sure the original is not modified." A const pointer would, wouldn't it? – Thomas Dec 02 '22 at 15:20
  • Assuming optional of reference exists, `postfix.emplace("2")` would store dangling pointer. – Jarod42 Dec 02 '22 at 15:20
  • @Thomas but then I couldn't replace its value if it's null? Or could I? – Alexander Soare Dec 02 '22 at 15:22
  • The argument would be a pointer-to-`const`, not necessarily `const` itself. But also didn't you say that in the Python you're actually creating a new variable and leaving the original alone and that's your desired behavior? Nothing would be stopping you from creating and using a new `std::string` local variable if the pointer is null. – Nathan Pierson Dec 02 '22 at 15:27
  • 1
    Sorry for the lack of precision. I meant a non-const pointer to a const value: `const std::string *postfix` or equivalently `std::string const *postfix`, rather than `std::string *const postfix`. (Rule of thumb: the `const` always applies to the thing directly before it, unless it's the first thing in the type.) – Thomas Dec 02 '22 at 15:27
  • @NathanPierson yeah, nothing s stopping me from creating and using a new `std::string` which is why I tacked on the requirement that I'd rather not be making another `postfix_`. But it's starting to feel like that's my best option, and to be honest, it's pretty good... – Alexander Soare Dec 02 '22 at 15:28
  • 1
    Ah, I see the problem now. You could reassign the non-const pointer, but you need a pointee with a sufficient lifetime. – Thomas Dec 02 '22 at 15:30
  • @Thomas I think I'm with you ish (you'll have to forgive my C++ noobisms). So if I have a non const pointer I can change it to point to something else. So I can do `postfix = &(some_computation_based_on_name(name))`. And that would be about equivalent to what's happening under the hood in the Python example. Your last comment makes me doubt though. – Alexander Soare Dec 02 '22 at 15:33
  • 2
    You'd need to do `postfix = &(something_that_lives_at_least_until_youre_done_using_postfix)`. So `auto calculatedSuffix = some_computation_based_on_name(name); postfix = &calculatedSuffix;` could work, because `calculatedSuffix` would be a local variable that lasts until `add_postfix` returns. But you can't just directly take the address of some temporary object returned by a function. – Nathan Pierson Dec 02 '22 at 15:35
  • @NathanPierson got it. Well this looks as close as I'm going to get to what I asked for. If you want to write an answer based on it please feel free and I'll accept. Otherwise I can. – Alexander Soare Dec 02 '22 at 15:38

4 Answers4

4

You do this with two functions:

std::string add_postfix(const std::string& name, const std::string& postfix) {
// whatever
}

std::string add_default_postfix(const std::string& name) {
return add_postfix(name, "2");
}

Or, if you're into overloading, you can write the second one as an overload by naming it add_postfix.

Pete Becker
  • 74,985
  • 8
  • 76
  • 165
  • Yeah someone commented this too. I have two such arguments so I'll need a little more overloading. If this is just "the way to do it", I'll leave it at that. Just coming from Python, it's more typing than I would like to do! – Alexander Soare Dec 02 '22 at 15:15
2

With your usage, value_or seems to do the job:

std::string add_postfix(const std::string& name,
                        const std::optional<std::string>& postfix)
{
    return name + postfix.value_or("2");
}

If you really want optional<T&>, optional<reference_wrapper<T>> might do the job.

std::string add_postfix(const std::string& name,
                        const std::optional<std::reference_wrapper<const std::string>>& postfix)
{
#if 1
    const std::string postfix_ = "2";
    return name + postfix.value_or(postfix_).get();
#else    // or
    return name + (postfix.has_value() ? postfix->get() : "2");
#endif
}

Demo

Jarod42
  • 203,559
  • 14
  • 181
  • 302
  • Yeah I'm thinking `value_or` looks good. Question about your snippet: if you go `const std::optional`, does that not mean the underlying string (if provided) is not protected from being modified? – Alexander Soare Dec 02 '22 at 15:26
  • `optional` does a copy of `T` internally, so original string is not modified. With [`std::reference_wrapper`](https://en.cppreference.com/w/cpp/utility/functional/reference_wrapper) you might simulate any of `optional` or `optional`. – Jarod42 Dec 02 '22 at 15:30
  • Ah yeah I don't want to be doing the copy. Looks like std::reference wrapper is the way to go. – Alexander Soare Dec 02 '22 at 15:39
2

One possibility is to use a std::string const* (a non-constant pointer to a const std::string) as a function argument.

std::string add_postfix(const std::string& name, std::string const* postfix = nullptr) 
{
  std::string derivedSuffix;
  if(!postfix) 
  { 
    derivedSuffix = some_computation(name); 
    postfix = &derivedSuffix;
  }
  return name + *postfix;
}

Some care is required with the details here. derivedSuffix needs to be an object that lasts at least as long as the pointer postfix refers to it. Therefore it cannot be contained entirely within the if(!postfix) block, because if it did then using *postfix outside of it would be invalid. There's technically still a bit of overhead here where we create an empty std::string even when postfix isn't nullptr, but we never have to make a copy of a std::string with actual values in it.

Nathan Pierson
  • 5,461
  • 1
  • 12
  • 30
  • I read the main comment thread. But I don't really see how this is useful. Is it the same as using `derivedSuffix`? I mean there is always a copy here. Nothing about your answer, I think what OP wanted was confusing and not necessary. – Federico Dec 02 '22 at 16:02
  • The copy being avoided is that if `postfix` is not `nullptr`, we don't make a copy of `*postfix`. – Nathan Pierson Dec 02 '22 at 16:06
1

You simply can write:

std::string add_postfix(const std::string& name, const std::string& postfix = "default value")
{
   return name + postfix;
}
NarekMeta
  • 103
  • 9
  • I should have been doubly clear (and I will edit my question to do so) but like I mentioned: I actually need to do some computation to get the 2. It's not a constant. – Alexander Soare Dec 02 '22 at 14:57
  • It was. If you look in the edit history you will see that. All I did was make it clearer after seeing your answer. – Alexander Soare Dec 02 '22 at 15:03
  • 1
    @AlexanderSoare: You can still have `void foo(int n = bar())` (cannot depend of other parameters though). – Jarod42 Dec 02 '22 at 15:53