-
Notifications
You must be signed in to change notification settings - Fork 217
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Policy for returning references and copies #90
Comments
A solution could be to differentiate between Iterables that are just the I'm not sure if partitioning Hana algorithms into different categories (via Personally, I expect any entity/iterable/sequence to be mutable in some El vie, 12 de jun de 2015 17:14, Louis Dionne [email protected]
|
One problem I have with returning references is the fact that it can create dangling references. Indeed, if struct Person {
BOOST_HANA_DEFINE_STRUCT(Person,
(int, age)
);
};
int main() {
auto m = members(Person{30});
// m is a Tuple holding a dangling reference to Person{30}.age
} This is annoying, because from using |
My concern in the above comment will be handled in #175. Going back to the original issue of returning references from functions like
@Manu343726, I think a variant of what you suggest is the right way to go. Seriously, we can't possibly force these functions to return values and expect C++ programmers to use the library. I also think it is mostly a matter of documenting the contract of those functions properly, since they already return references in most cases (I think all of them do except While I wouldn't introduce another namespace, it would be possible to say something along the lines of: "This function must return a reference to the element whenever the object it is called on is a container with actual storage." This is not very precise mathematically, but everybody will understand. Also, this is mostly directed to users since we don't make assumptions about the value category of the things returned by those functions in Hana AFAICT. So while precision is nice, it will not actually undermine the internal consistency of Hana if this is not mathematically precise. However, there is still the issue of what should we return from a temporary. For example, what should auto x = hana::at_c<0>(hana::make_tuple(1, 2, 3)); If we do like they do for It seems to me like returning a |
From this Stackoverflow answer, there does not seem to be a clear cut best solution. It is clearly acknowledged that returning an rvalue reference can lead to dangling reference problems, yet the standard still does it. I'm not sure what is the best stance to adopt here. |
Addresses this StackOverflow question: http://stackoverflow.com/q/32702383/627587 And also partially addresses #90.
Addresses this StackOverflow question: http://stackoverflow.com/q/32702383/627587 And also partially addresses #90.
For a temporary |
No, I have not found anything. If you do, let me know. I think the simplest thing would be to ask Howard Hinnant directly. |
Sorry if this is a little late in the conversation. In the OP example with the struct, I think it would be beneficial to change the usage to be more like an asynchronous callback interface that you would expect in something like javascript. That way the reference would only be available within the context of the callback. #include <boost/hana.hpp>
namespace hana = boost::hana;
using namespace boost::hana::literals;
struct at_t {
template <typename S, typename I, typename Fn>
constexpr auto operator()(S&& s, I, Fn&& fn) const {
return hana::unpack(std::forward<S>(s),
[&](auto&&... x) {
return std::forward<Fn>(fn)(
hana::arg<I::value + 1>(std::forward<decltype(x)>(x)...)
);
}
);
}
};
constexpr at_t at{};
int main() {
auto john = hana::make_tuple(30);
at(john, 0_c, [](auto& arg) { arg = 35; });
BOOST_HANA_RUNTIME_ASSERT(john[0_c] == 35);
}
This sort of interface would also make wrapping a reference in an |
@ldionne I know you probably realize this, but I'd like to make a clarification on your example from earlier in this thread: auto x = hana::at_c<0>(hana::make_tuple(1, 2, 3)); As far as I know, this statement does not actually involve any reference dangling. I bring this up not to be pedantic, but because your comment about the "dangling Before drafting a question for Howard Hinnant, I wanted to toy around in Wandbox to fill any holes in my understanding of rvalue references, as well as This little program's output depends on a colloquial expectation of what might happen when a dangling reference is accessed within the same call frame as the referenced object's destruction - in other words, "don't try this at home, kids!" There's no legitimate reason why this program doesn't segfault (or worse), and it very well may if you run it. For the Hana discrepancy, see case 2 (Wandbox link here): #include <boost/hana.hpp>
#include <iostream>
#include <string>
#include <type_traits>
#include <utility>
#include <tuple>
namespace hana = boost::hana;
const char* const undefined = "Undefined behavior! You're lucky to read this!";
const char* const dflt = "default";
const char* const copied = "copied";
const char* const moved = "moved";
struct foo {
static int default_count;
static int copy_count;
static int move_count;
const char* value;
foo()
: value{ dflt } {
default_count++;
}
foo(const foo& f)
: value{ f.value == undefined? undefined : copied } {
copy_count++;
}
foo(foo&& f)
: value{ f.value == undefined? undefined : moved } {
move_count++;
}
~foo(){
value = undefined;
}
};
int foo::default_count = 0;
int foo::copy_count = 0;
int foo::move_count = 0;
void reset_counts() {
foo::default_count = 0;
foo::copy_count = 0;
foo::move_count = 0;
}
void print_result(const char* test_case, const char* s) {
std::cout << test_case << std::endl;
std::cout << s << std::endl;
std::cout << "defaults: " << foo::default_count << std::endl;
std::cout << "copies: " << foo::copy_count << std::endl;
std::cout << "moves: " << foo::move_count << std::endl;
std::cout << std::endl << std::endl;
reset_counts();
}
auto&& get_hana(){
return hana::at_c<0>(hana::make_tuple(foo{}));
}
auto&& get_std(){
return std::get<0>(std::make_tuple(foo{}));
}
// pls forgive
#define GET_HANA hana::at_c<0>(hana::make_tuple(foo{}))
#define GET_STD std::get<0>(std::make_tuple(foo{}))
void case_1() {
std::cout << std::endl << "CASE 1" << std::endl << std::endl;
// The tuple temporary is destroyed at the semicolon in both
// get_hana and get_std (and anywhere else in C++). Reference
// forwarding can't save us from that. (Neither does inlining,
// which is probably obvious to those reading, but perhaps
// worth noting)
{
auto dangle = get_std().value;
print_result("STD TEMPORARY DANGLE", dangle);
// value is accessed before the end of the expression, and
// therefore before the tuple temporary is destructed as well
auto no_dangle = GET_STD.value;
print_result("STD TEMPORARY NO DANGLE", no_dangle);
}
{
// Hana has the same behavior as the standard libary here
auto dangle = get_hana().value;
print_result("HANA TEMPORARY DANGLE", dangle);
auto no_dangle = GET_HANA.value;
print_result("HANA TEMPORARY NO DANGLE", no_dangle);
}
}
void case_2() {
std::cout << std::endl << "CASE 2" << std::endl << std::endl;
{
// Move-constructing a foo object named move_dangle. Since the
// tuple temporary destruction is sequenced before the function
// return, the rvalue reference passed to the move constructor
// of foo is invalid. The ?: expression in the move constructor
// informs us, colloquially, of the undefined behavior we induce here
auto move_dangle = get_std();
print_result("STD MOVE DANGLE", move_dangle.value);
auto move_no_dangle = GET_STD;
print_result("STD MOVE NO DANGLE", move_no_dangle.value);
}
{
// Hana does not have the same behavior as the standard library here
std::cout << "*** This doesn't actually dangle... "
"Why does the foo move constructor receive " << std::endl
<< " a valid rvalue reference here? ***" << std::endl;
auto move_dangle = get_hana();
print_result("HANA MOVE DANGLE", move_dangle.value);
auto move_no_dangle = GET_HANA;
print_result("HANA MOVE NO DANGLE", move_no_dangle.value);
}
}
void case_3() {
std::cout << std::endl << "CASE 3" << std::endl << std::endl;
// these are obvious, but lets include them for fun :)
{
auto&& double_dangle = get_std();
print_result("STD DOUBLE DANGLE", double_dangle.value);
auto&& single_dangle = GET_STD;
print_result("STD SINGLE DANGLE", single_dangle.value);
}
{
auto&& double_dangle = get_hana();
print_result("HANA DOUBLE DANGLE", double_dangle.value);
auto&& single_dangle = GET_HANA;
print_result("HANA SINGLE DANGLE", single_dangle.value);
}
}
int main() {
case_1();
case_2();
case_3();
return 0;
}
Please correct me if the code above indicates a misunderstanding on my part. The things I learned here tell me that the fact that It seems to me the standard's rationale for the behavior of I have not yet written a question to Howard Hinnant, since I've unconvinced myself of the idea that there is anything wrong with the standard implementation. We could try to ping him at @ ... HowardHinnant. @ricejasonf That would certainly work. My gut tells me that requiring a lambda argument asks a lot of boilerplate from the user. If we're going to allow the user to determine the value category of the return type, I'd be more inclined to take a tag as a template argument - something like I think the real problem here is that
Can we Edit: I realize now that my code comments referred to the destruction of the foo temporary, which was somewhat misleading because the original foo temporary is moved into the tuple, which means the original foo's destruction is inconsequential. It's really the destruction of the tuple temporary, and its elements along with it, that affects the example code. I updated the code comments in this thread, but I left the Wandbox code alone. The meaning/purpose of both remains unchanged. Edit 2: I believe the culprit of the [possible] issue in case 2 can be narrowed down to the tag dispatch result that handles this apply call. I'm not familiar enough with BOOST_HANA_DISPATCH_IF to continue mentally debugging this tonight. |
First of all, I don't yet understand why the behaviour of Coming back to this: auto x = hana::at_c<0>(hana::make_tuple(1, 2, 3)); As far as I understand, Otherwise, it is also possible that the expression must be interpreted as being int i;
new (&i) int(hana::at_c<0>(hana::make_tuple(1, 2, 3))); // <-- in-place new expression (sort of?) In which case the temporary @ricejasonf Regarding the use of callbacks to access elements; I think this is way too restrictive and would lead to hard-to-read code in many cases. I think we can settle on a good-enough solution that will not require going through all these hoops just for basic functionality like getting the n-th element of a sequence. |
Would you happen to know why the Standard Committee decided to have Also, am I right to think that std::string s = std::get<0>(std::make_tuple(std::string{"abcdef"})); causes |
Here are links to the papers. They are light on motivation and don't discuss the possibility of returning by value: And no, the temporary |
Thanks a lot Howard. Out of curiosity, I will try to reach out to Alisdair to inquire about the rationale for Indeed, from my updated understanding, I think the only case in which it would be safer to return a If this is correct, and if this is about the only case in which it is safer to return a Does that make sense? |
Since it seems like everybody is on GitHub these days, let's ask Alisdair right here :-) @AlisdairM, would you happen to know the rationale for returning a rvalue-reference from |
I went through a similar design decision with rvalue-string + lvalue-string. It originally returned an rvalue reference to the left argument. I later changed it to return by value, moving from the modified left argument because of the dangling reference possibility. In this case I had the advantage that I knew the move would be cheap. In the case of |
@ldionne Yes, and I agree with your conclusion. I applied Howard's state machine to the case 2 example. It looks like Hana is dangling after all, like the standard (Wandbox). Hana's result here is usually garbage memory, which leads me to believe the original output was caused by the wrath of the UB gods. I apologize for the confusion. @HowardHinnant Thank you for the links and example. |
Currently, Hana does not make any guarantee about what is returned from methods like
at
orat_key
. Indeed, the documentation forat_key
just saysThis makes no guarantee that a reference to the value in the map should be returned. Indeed, the following code currently fails because
at_key
returns a copy of the value, which is a temporaryint
:The error is:
This causes serious usability problems. On the other hands, if we had a special
Iterable
generating values on the fly (for example an infiniteIterable
generating the fibonacci sequence), we wouldn't be able to return a reference. Hence, the resolution is not exactly clear.Another related problem is to determine a policy for when to return a container of references. For example, consider the following code, which was submitted to me in a private email:
This is because
members
returns a container holding copies of the members of the struct, not references to it. While it seems that it would be more useful to return a sequence of references, it would also create lifetime issues when thePerson
is a temporary.The text was updated successfully, but these errors were encountered: