10

I'm implementing a container with a proxy iterator/reference type similar to std::vector<bool> and clash into the following issue, which I proceed to exemplify with std::vector<bool> (this question is not about std::vector<bool>!):

#include <vector>
#include <type_traits>
int main() {
  using namespace std;
  vector<bool> vec = {true, false, true, false};
  auto value = vec[2];  // expect: "vector<bool>::value_type"
  const auto& reference = vec[2]; // expect: "vector<bool>::const_reference"

  static_assert(is_same<decltype(value), vector<bool>::value_type>::value, 
                "fails: type is vector<bool>::reference!");
  static_assert(is_same<decltype(reference), 
                        vector<bool>::const_reference>::value,
                "fails: type is const vector<bool>::reference&!"); 

  /// Consequence:
  auto other_value = value; 
  other_value = false; 
  assert(vec[2] == true && "fails: assignment modified the vector");
  • Is there a way to implement a proxy type such that both static assert's pass?

  • Are there any guidelines about how to deal with this issue when implementing such a container?

Maybe by using a conversion operator to auto/auto&/auto&&/const auto...?

EDIT: reworked the example to make it more clear. Thanks to @LucDanton for his comment below.

gnzlbg
  • 7,135
  • 5
  • 53
  • 106
  • You do know that [`std::vector`](http://en.cppreference.com/w/cpp/container/vector_bool) is a specialization that doesn't work quite like any other [`std::vector`](http://en.cppreference.com/w/cpp/container/vector)? – Some programmer dude Nov 21 '13 at 15:16
  • 1
    @JoachimPileborg yes, `std::vector` is not a std container. It illustrates the usage of proxy iterator/reference types and their pitfalls very clearly. I'm basically asking if there is any way to avoid them. – gnzlbg Nov 21 '13 at 15:16
  • You should add the [C++] tag as well, see [the C++11 tag info page](http://stackoverflow.com/tags/c%2b%2b11/info) -- but you'd had to remove one of the other tags then. – dyp Nov 21 '13 at 15:33
  • I haven't seen `decltype(auto) var = init;`. Is it similar to `auto&& var = init;`? – aschepler Nov 21 '13 at 18:44
  • Found my own answer: http://en.wikipedia.org/wiki/C%2B%2B14#Alternate_type_deduction_on_declaration – aschepler Nov 21 '13 at 18:54
  • Well hate to sound like a noob, but `decltype((bool)value)` will have the static_assert pass on `clang`, although I doubt it has anything to do with the question. –  Nov 21 '13 at 18:56
  • @remyabel correct, that would make the `static_assert` pass. The question is if there is a way to make it pass _without_ doing an explicit conversion. The user should not need to know that he is not getting a bool but a proxy type. – gnzlbg Nov 21 '13 at 19:13
  • 2
    When returning by value (likely to be the case when returning proxies), `auto` and `decltype(auto)` coincide. If you want to make them differ, you'll run into iffy considerations with lifetimes (e.g. where will the proxy object reside, and for how long?). – Luc Danton Nov 21 '13 at 19:28
  • @LucDanton yes exactly. I've added your point to the question and another example to clarify the problem. Basically I need something like conversion operators to `auto`, `auto&`, ... Then I'm in control of where the proxy objects reside so life time issues disappear. – gnzlbg Nov 22 '13 at 10:31
  • I've found this paper which might implement such a feature so I don't think a good solution is possible yet: http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2013/n3748.pdf – gnzlbg Nov 22 '13 at 10:44

2 Answers2

5

Proxies and auto don't interact well, precisely because auto reveals things about the types that were supposed to stay hidden.

There have been some requests for interest for operator auto-style things (basically, "when deducing me as a type, use this type instead"), but AFAIK none of them even made it to an official proposal.

The other problem is that vector<bool> is unexpected because it's the only instantiation of vector that uses proxies. There have been other preliminary proposals calling for vector<bool> to be deprecated and eventually revert to being non-special, with a special-purpose bitvector class introduced to take its place.

user2672165
  • 2,986
  • 19
  • 27
Sebastian Redl
  • 69,373
  • 8
  • 123
  • 157
  • 1
    I'm writing a container that needs to return a proxy. Is there anything I can do to save my users from these pitfalls? – gnzlbg Nov 22 '13 at 12:34
  • 2
    No. Proxies aren't fully transparent, and `auto` makes them even less so than they used to be. It's a weakness in the language. – Sebastian Redl Nov 22 '13 at 12:38
3

As is well-known, vector<bool> has a non-generic interface compared to the primary template vector<T>.

The relevant differences are that the nested types reference and const_reference are a typedef for T& and T const& in the general case, and to the proxy class reference and the value type bool for vector<bool>.

When accessing vector elements, it is also important to remember that the constness of the vector object determines whether a reference or const_reference is being returned by operator[]. Furthermore, auto will drop the reference qualifiers whereas decltype will keep those.

Let's look at a non-const / const vector of bool / int, and use auto, decltype(auto) and auto const& (plain auto& will lead to live-time issues for proxies). You get the following behavior:

#include <vector>
#include <type_traits>
#include <typeinfo>
#include <iostream>
#include <ios>

int main() {
  using namespace std;

  vector<bool> vb = { true, false, true, false };
  vector<int > vi = {    1,     0,    1,     0 };

  auto vb2 = vb[2];             // vector<bool>::reference != bool
  auto vi2 = vi[2];             // int
  decltype(auto) rvb2 = vb[2];  // vector<bool>::reference
  decltype(auto) rvi2 = vi[2];  // int&
  auto const& crvb2 = vb[2];    // vector<bool>::reference const& != bool const&
  auto const& crvi2 = vi[2];    // int const&

  auto ovb2 = vb2;
  ovb2 = false;                 // OOPS ovb2 has reference semantics
  cout << boolalpha << (vb[2] == true) << "\n";

  auto ovi2 = vi2;
  ovi2 = 0;                     // OK, ovi2 has value semantics
  cout << boolalpha << (vi[2] == 1) << "\n";

  static_assert(is_convertible<decltype(vb2),   vector<bool>::value_type>::value, "");  
  static_assert(is_same       <decltype(vi2),   vector<int >::value_type>::value, "");
  static_assert(is_same       <decltype(rvb2),  vector<bool>::reference>::value, "");  
  static_assert(is_same       <decltype(rvi2),  vector<int >::reference>::value, "");
  static_assert(is_convertible<decltype(crvb2), vector<bool>::const_reference>::value, "");  
  static_assert(is_same       <decltype(crvi2), vector<int >::const_reference>::value, "");

  vector<bool> const cvb = { true, false, true, false };
  vector<int > const cvi = {    1,     0,    1,     0 };   

  auto cvb2 = cvb[2];            // vector<bool>::const_reference == bool
  auto cvi2 = cvi[2];            // int
  decltype(auto) rcvb2 = cvb[2]; // vector<bool>::const_reference == bool
  decltype(auto) rcvi2 = cvi[2]; // int const&
  auto const& crcvb2 = cvb[2];   // vector<bool>::reference const& != bool const&
  auto const& crcvi2 = cvi[2];   // int const&

  static_assert(is_same       <decltype(cvb2),   vector<bool>::value_type>::value, "");  
  static_assert(is_same       <decltype(cvi2),   vector<int >::value_type>::value, "");
  static_assert(is_same       <decltype(rcvb2),  vector<bool>::const_reference>::value, "");  
  static_assert(is_same       <decltype(rcvi2),  vector<int >::const_reference>::value, "");
  static_assert(is_convertible<decltype(crcvb2), vector<bool>::const_reference>::value, "");  
  static_assert(is_same       <decltype(crcvi2), vector<int >::const_reference>::value, "");

  auto ocvb2 = cvb2;
  ocvb2 = false;                 // OK, ocvb2 has value semantics
  cout << boolalpha << (cvb[2] == true) << "\n";

  auto ocvi2 = cvi2;
  ocvi2 = 0;                     // OK, ocvi2 has value semantics
  cout << boolalpha << (cvi[2] == 1) << "\n";  
}

Live Example

Note that for a non-const vector<bool>, using auto on operator[] will give you a reference proxy that does not have value semantics. Using a const vector<bool> will avoid that. I don't see how this can be solved in any other way.

The auto const& is behaviorally equivalent but has a is_convertible rather than is_same inside the static_assert. I think this is the best one can do.

Note that for general iteration and STL algorithms on proxy containers, things are not so bleak. See Hinnant's column on this.

TemplateRex
  • 69,038
  • 19
  • 164
  • 304
  • the problem is that `auto value` has type `vector::reverence` instead of `vector::value_type`. Only `auto& reference` should have type `vector::reference` but right now it has type `vector::reference&` which is also wrong. – gnzlbg Nov 22 '13 at 10:35
  • @gnzlbg yes and that is unavoidable because `auto` follows the rules of template argument deduction which in turn does not consider conversion sequences (such as the `vector::reference` to `bool` user-defined conversion). But it is also no big deal as you can relax your assertion to only test for type convertability rather than type equality. This is what a proxy is all about: using something else behind your back without getting in your way. – TemplateRex Nov 22 '13 at 11:36
  • 1
    "auto v = vec[1]; v = other;" doesn't modify the container value *unless* you are getting a proxy. In that case it modifies the value in the container! So yes, the fact that auto follows TAD defeats the purpose of using a proxy because it does get in your way in non obvious ways. Furthermore, the user *really* needs to know that he is getting a proxy in order to avoid this pitfall. – gnzlbg Nov 22 '13 at 12:18
  • I've added a _consequence_ section to the question that explains this. – gnzlbg Nov 22 '13 at 12:31
  • 1
    @gnzlbg tnx, I updated my answer with a systematic exploration of the discrepancies between `vector` and `vector`. – TemplateRex Nov 22 '13 at 14:28