if (foo(p.release()) == 0) { ... }
you generally need to do if (foo(p.get()) == 0) { p.release(); ... }
Instead of reducing bugs, these might be introducing new footguns. if (foo(p.release()) == 0) { ... }
you generally need to do if (foo(p.get()) == 0) { p.release(); ... }
Instead of reducing bugs, these might be introducing new footguns. void c_api_that_returns_via_an_out_parameter(T** ret) {
*ret = new T();
}
T* p;
c_api_that_returns_via_an_out_parameter(&p);
Then you might try to change this to use a smart pointer as: std::unique_ptr<T> p;
c_api_that_returns_via_an_out_parameter(&p);
But this won't compile because you can't take the address of a `std::unique_ptr<T>` and then pass it where a `T**` is expected. So instead you need to do something like this: T* tmp = 0;
c_api_that_returns_via_an_out_parameter(&tmp);
std::unique_ptr<T> p(tmp);
That can be a bit of a pain. These new functions serve as adapters to make this easier. They take in a `std::unique_ptr<T>` and provide a `T*` that you can pass to the C API: std::unique_ptr<T> p;
c_api_that_returns_via_an_out_parameter(std::out_ptr(p));
No temporary needed.If your C API only ever passes and returns `T*` and never uses `T**` then I think that you don't need these new functions and you can continue to do things the current way. For instance, if you have a C API that allocates a new object and then returns a pointer to it then you can take this return value and immediately store it into your smart pointer:
std::unique_ptr<T> p(c_api_that_returns_a_pointer());
And if you have a C API that takes a `T**` and then deallocates it: c_api_that_deallocates(p.release());
Although really your smart pointer should be doing this for you so you'd never need to do it explicitly. If you're ever manually calling `.release()` then that indicates a place where the smart pointer abstraction has broken down. You want to minimize or eliminate that. Note that in the examples using the new functions there are no explicit calls to `.release()` anywhere and this is a good thing.I can't recall ever seeing a C API that conditionally deallocates the pointer passed in. Do you have an example? Most C APIs that I'm familiar with have one function that allocates a new object and a separate function that unconditionally deallocates it, acting analogously to `malloc` and `free`.
Yes, that's life when you're at the boundary with a C API.
> I can't recall ever seeing a C API that conditionally deallocates the pointer passed in. Do you have an example?
realloc()?
Generally when you have an in-out parameter, it's doing more than just releasing resources, so there's a potential for failure. And in the failure case, the caller will often retain ownership of any pointers it passed. In fact, off the top of my head, I can only think of one function that is contrary to this, and that is DeferWindowPos() in Windows, which invalidates the input handle even on failure.
OK, I'll give you that one, but that's a rather unusual case. It has complicated semantics. It is unlikely to interoperate well with any smart pointer without careful, manual work.
It doesn't take a `T**` so it doesn't seem relevant to discussions of std::out_ptr though? You won't be able to use std::out_ptr with realloc so there's no chance of making a mistake with it.
> Generally when you have an in-out parameter, it's doing more than just releasing resources
I'm still confused. What function do you have that is both releasing resources and also doing other work which might fail? Even something like `fclose` which needs to flush buffers and then release resources will always release the resources, even if the flush fails.
I'm more familiar with APIs that have:
- One (or more) functions to explicitly allocate the object
- Many functions that do work with that object and that might fail but that will never deallocate the object
- One function to explicitly deallocate the object
For example, SQLite has sqlite3_open to create a new database object, sqlite3_prepare, sqlite3_exec, etc. to operate on the database object, and sqlite3_close to deallocate the database object.
The problem is the same, it just happens to not take a T**. The problem is the same for any function that takes T**, I just couldn't think of one off the top of my head. I listed some below now. But also note that the absence of many APIs taking T** would also be an argument for these smart pointers not being so useful, so it's not really a counterargument!
> What function do you have that is both releasing resources and also doing other work which might fail?
See for example getdelim() or even asprintf(). asprintf() isn't guaranteed to return a valid pointer due to an error, so you can't free it unconditionally. getdelim() also isn't guaranteed to free the input if there is an I/O error, so you can't release that unconditionally either.
It would be helpful if you try to list some functions you believe you can use std::inout_ptr on.
std::unique_ptr<char, free_deleter> line;
size_t len = 0;
while ((read = getdelim(std::inout_ptr(line), &len, delim, fp)) != -1) {
// ...
}
If the getdelim call ends up calling realloc and succeeding then we want to forget about the old pointer and not call `free` on it. We want to remember the new pointer and call `free` on that when we're done with the loop. That's what std::inout_ptr gets for us. It calls release() before the getdelim call and then calls reset() with the new value after the getdelim call.If there is an I/O error or a realloc failure at some point in the file then `line` is left unmodified, pointing to the existing buffer. We still need to free that so keeping it under the control of the smart pointer is the right thing to do.
This seems like a perfect fit for std::inout_ptr.
The situation asprintf() is unfortunate. If it had been designed to set the out parameter to NULL on error or just to guarantee that it was unmodified on error then it would work with std::out_ptr. Unfortunately they decided that the out pointer would be instead be undefined on error so it is not safe to use with std::out_ptr.
Same thing could be/has been said about std::string_view or std::span but here we are.
Like you said, a bare pointer could mean anything. This at least standardizes this particular footgun in question (which means one less reason to expose bare pointers in your API) so we are supposed to pay extra attention when we see one of those, I guess?
It's the first time I'm hearing this, and it doesn't sound like an opinion I've ever had. Those are just range-checked pointers; their ownership semantics aren't any different from those of raw pointers, and they're no less safe than raw pointers. In contrast these actually do have weird ownership semantics, and they have a propensity to introduce safety bugs into the common use cases that didn't exist before.
Say that you have some `Person` class with a `get_name()` method that returns the person's name. Then maybe you write some code like this to use it:
Person person;
// ...
std::string_view name(person.get_name());
// Use `name` here
Is this code safe? Well it depends on the implementation of `Person::get_name`. Maybe it looks like this: std::string& Person::get_name() {
return m_name;
}
Then the code should be OK because the call will return a reference to a member of `person` and `person` will outlive `name`.But what if instead the implementation looks like this:
std::string Person::get_name() {
return m_first_name + " " + m_last_name;
}
Now you're in trouble. A temporary string will be created and the string_view will refer to that string. That temporary gets destroyed at the end of the statement and then `name` is dangling. By the time you get to use `name` it is already broken.It is very easy to make this kind of mistake. Much easier than with pointers IMO since you usually have to explicitly take a pointer to something and you can't directly get a pointer to a temporary (`std::string* p = &person.get_name()` is an error if `Person::get_name` returns `std::string`).
By contrast, getting a const reference to a temporary and then constructing a string_view out of it is not an error or even a warning. It is all done implicitly so there is no indication in the code that it is even happening. The only difference between code that is correct and code that is incorrect is a single ampersand far away in some header file.
This type of mistake probably won't get caught in code review. How often does a person really go track down and examine a header file when reviewing a pull request? Probably they're only going to look at the actual files that are being changed.
Even worse, say that `Person::get_name` is using the first implementation so the code is correct and working. But later on somebody does a refactor such `Person::get_name` uses the second implementation. Now they've broken things but they're unaware of this fact. They want to be responsible and find any problems that they might have introduced. The first step is to run a build and see if there are any complaints from the compiler. That is successful and the compiler produces no warnings.
Encouraged by this they then run all the tests. And all the tests pass! Reading memory just after it has been freed will often give you back the last thing that was stored there so the tests see the values that they expect. So with a successful build, a successful run of all the tests, and no problems spotted in code review, they confidently merge the change. And now you've got a use-after-free in your code that will eventually cause a problem.
Yes, using Address Sanitizer will catch this. But it is still a pretty easy way to introduce a bug into your program. Code that does not look at all suspicious can be completely broken.
Yes, it’s one more thing nudging the programmer toward unsafe use, but this is C++ we’re talking about: only the strong survive.
In my opinion, `string_view` and `span` are otherwise wonderfully much-needed concepts (that arguably should have been built into the language itself, like Rust slices) that should never have been designed to allow implicitly creating views of temporaries. Disabling implicit conversion/construction from rvalue references is quite possible in C++17 and beyond, and very effectively prevents accidentally implicitly viewing temporaries just fine (I’ve done this myself in some enhanced span-like classes I’ve written), so I’m really not sure why string_view was designed this way.
https://en.cppreference.com/w/cpp/string/basic_string/operat...
void frob(std::string_view sv);
std::string a = "a";
std::string b = "b";
frob(a + b);
This is allowed today and does not have any problems that I'm aware of. You could work around this by changing the call to: frob(std::string_view(a + b));
But that feels cumbersome. I think the goal of std::string_view was to be useful as a parameter for a function so that you could pass in anything remotely string-like and it would implicitly convert and do what you meant. Requiring explicit conversions at the call sites would go against that goal.Another thing that was desired was the ability to take an existing function that takes a `const std::string&` and change it to instead take a `std::string_view` and not have to update any of the callers. If some callers that worked with the old function are going to need an update to work with the new function then it gets a lot harder to change the function. Or maybe impossible if you don't control all of the callers.
I don't see how you can get both the ease-of-use and the safety short of reinventing something like Rust's lifetimes.
I know anything other than switching to Rust is a losing battle of compromises in some sense, but many of us working with huge codebases written in C++ don’t have the luxury of that choice being unavailable.
Sure you mist avoid a few things but there are alternatives on what to do instead/not what to do.
- you can use string_view more conservatively
- you can stick to value semantics.
- use spans in things that do not escape.
- use smart pointers.
- do not capture escaping lambdas that have capture by reference.
If you go wild raw pointers willy-nilly around, then yes, you are gonna have a plague of sh*t because you are an incompetent using C++. I do not expect people to play the violin or drive a car without a minimal of training.
So suggesting a disciplined usage of c++ sounded like a hard sell. It's helpful that you elaborated on the details.
The real problem is that in C++ temporaries are destructed at the end of the expression, not at the end of the block that contains them. With the latter rule the example and many other cases will be safe.
When I see std::string_view, nothing tells me that this may lead to memory corruption because the underlying storage may go away at any time (and the compiler will happily let this happen). New C++ features simply should not create new memory corruption footguns in addition to the existing ones, embarrassments like iterator invalidation are already bad enough (and worse than typical C memory management issues, because in C such issues are usually 'in your face', while C++ hides them beneath layers upon layers of stdlib abstractions).
That's not true. The word "view" quite explicitly tells you that it's a view... into something else, whose existence is generally independent of you (just like in real life!). If anything, it's clearer than an asterisk.
> (and the compiler will happily let this happen).
Just like with pointers and iterators. There's no difference here.
> When I see a raw pointer in C or C++ code I know automatically that gotchas are involved and I need to be extra careful.
You "know" this for iterators too. There's no reason why you can't "know" it for views too. It's not like they're introducing a new ownership concept here - these are just bounded versions of iterators and pointers.
C++ is a huge sinking ship...