Emily L.
Emily L.

Reputation: 5931

How to properly declare a const pointer from non-const iterator to pointer

Background

I'm implementing a template filtering iterator. Given a start and end iterator of any type, this iterator will iterate over the range and skip over any elements for which a unary predicate returns false. Naturally I want this unary predicate to always have a const argument as to avoid modification of the backing container by the predicate.

The backing iterator can be an iterator for any type and container. It can be basic types, pointers, references, classes. Anything really.

I run into a problem where I'm unable to declare a std::function to have the correct, const declared argument based on the template argument iterator. I have distilled a minimal code example that illustrates the problem.

Code

#include <vector>
#include <functional>

typedef std::vector<int*> vec_type;
typedef std::function<void(const vec_type::iterator::value_type&)> func_type;

void foo(vec_type& a, func_type f){
  for (auto it = a.begin(); it != a.end(); ++it){
    f(*it);
  }
}

int main(int, char**){
  vec_type v;
  int a = 3;
  int b = 4;
  v.push_back(&a);
  v.push_back(&b);
  foo(v, [](int* x){*x = 0; });
  return 0;
}

I'm expecting a compile error on the lamda because int* should be const int* but both GCC 4.8.1 and VS2013 allow it and happily modify what I thought was a going to be const.

Upvotes: 2

Views: 808

Answers (2)

Yakk - Adam Nevraumont
Yakk - Adam Nevraumont

Reputation: 275385

A container of pointers is not modified when you modify what the pointers point to: the container owns the pointers, not the pointed-to.

But I understand -- sometimes you want constness to propogate down pointers.

This is a bit of template metaprogramming which should take any non-const pointer and make it const, as well as everything else becomes as const as it can. It operates recursively, handling references (r and l value) and references to pointers to pointers to pointers to pointers, with or without const or volatile modifiers pretty much anywhere.

template<class T>struct tag{using type=T;};

template<class X>
struct make_very_const:tag<const X> {};
template<class X>
using make_very_const_t=typename make_very_const<X>::type;
// makes code below easier to write (namely, the pointer code):
template<class X>
struct make_very_const<const X>:tag<const make_very_const_t<X>> {};
template<class X>
struct make_very_const<volatile X>:tag<const volatile make_very_const_t<X>> {};
template<class X>
struct make_very_const<const volatile X>:tag<const volatile make_very_const_t<X>> {};
// references:
template<class X>
struct make_very_const<X&>:tag<make_very_const_t<X>&>{};
template<class X>
struct make_very_const<X&&>:tag<make_very_const_t<X>&&>{};
// pointers:
template<class X>
struct make_very_const<X*>:tag<make_very_const_t<X>*const>{};
// std::reference_wrapper:
template<class X>
struct make_very_const<std::reference_wrapper<X>>:tag<std::reference_wrapper<make_very_const_t<X>>const>{};

live example

which is a lot of verbage. The reason why it is so verbose is that there is no easy way to match on "type modifiers" (pointer, const, volatile, reference etc), so you end up having to be really specific and verbose.

But it gives you a clean way to do it at point of use:

typedef std::vector<int*> vec_type;
typedef std::function<void(make_very_const_t<vec_type::iterator::value_type&>)> func_type;

which spews const all over everything in a way that should be implicitly convertible to.

Now, even this isn't fully effective. A std::vector< std::vector<int*> > will not protect the pointed to inner int from being modified. The above relies on the ability to implicitly convert to the more const case -- so to pull this off, we'd have to write a const-forcing wrapper around a nearly arbitrary container that enforces element access to go through the above transformation. This is difficult.

In general, if you want an int* where the object being const makes the pointed to object const, you need a smart pointer that enforces that requirement. N4388 is a proposal to add a wrapper that does that to the standard, if not exactly for this purpose.

Upvotes: 3

milleniumbug
milleniumbug

Reputation: 15814

Your container stores int*s. Your function accepts "const reference to int*". This means that the pointer points to the mutable data. The data can be happily modified because you allowed it. Note the distinction between the "degrees of constness" - you can't change what the pointers point to, but you can modify the ints they point to. So, to fix this problem, either your function must accept "const reference to const int*" or const int*.

typedef std::function<void(const int*)> func_type;

...or, if you want it slightly more generic (see Yakk's answer for even more generic solution):

#include <type_traits>
typedef std::vector<int*> vec_type;
typedef
std::add_pointer<
  std::add_const<
    std::remove_pointer<
      vec_type::iterator::value_type
    >::type
  >::type
>::type value_t;
typedef std::function<void(const value_t&)> func_type;

Upvotes: 2

Related Questions