0

I have the following class:

class Data;
class A
{
public:
    A(Data& _data) : data(_data) {}
    Data& getData() {return data;}
    const Data& getData() const {return data;}
private:
    Data& data;
};

Now imagine I need to keep not one, but multiple instances of Data. I keep them in a vector of reference wrappers, but I would also like to keep the const correctness: pass the data as unmodifiable in const context.

class A
{
public:
    void addData(Data& _data) {data.push_back(std::ref(_data));}
    const std::vector<std::reference_wrapper<Data>>& getData() {return data;}
    //doesn't compile
    //const std::vector<std::reference_wrapper<const Data>>& getData() const {return data;}
private:
    std::vector<std::reference_wrapper<Data>> data;
}

How to implement this without having physical copying of the data? I.e. I don't want to return a copy of the vector by value and I don't want to keep two separate vectors in class A. Both are performance-impacting solutions for what is basically just a semantic problem.

Jakub Zaverka
  • 8,816
  • 3
  • 32
  • 48
  • Do you want to keep the `Data` object in the vector mutable inside `A`? – NathanOliver Sep 18 '19 at 13:24
  • 2
    Make your own const-propagating replacement for `reference_wrapper`? Or, instead of returning a reference to the entire vector, make a (const-correct) function that returns a reference to a single vector element (by index). – HolyBlackCat Sep 18 '19 at 13:25
  • @NathanOliver yes, I want to be able to also pass the data as non-const. – Jakub Zaverka Sep 18 '19 at 13:34
  • I would simply use a reinterpret cast to a reference to a vector of const reference wrappers. – Arne J Sep 18 '19 at 13:48
  • The best ting I can think of is to make a proxy pobject that holds a reference to the vector and only exposes a `const` from there. – NathanOliver Sep 18 '19 at 13:51
  • @ArneJ that's undefined – Caleth Sep 18 '19 at 15:35
  • 2
  • @LightnessRacesinOrbit @Caleth I would be surprised if casting ever produced a problem. But as a more correct alternative, I would suggest programming the Visitor Pattern in class `A` : `void accept(Visitor & visitor)` with visitor having the method `virtual void visit(const Data & data) = 0;` – Arne J Sep 20 '19 at 07:15
  • Don't expose concrete containers used to implement your class. Instead, make the class itself a container, forvarding operations to the internal implementation. – n. m. could be an AI Sep 20 '19 at 07:53
  • @ArneJ who says the users of `A` will access each element once, in order? – Caleth Sep 20 '19 at 08:22
  • @Arne I wouldn't be at all surprised if it produced a problem. The implementations could be completely different! Compilers are crazy complex and committing sins of UB can result in the most "bizarre" happenstances. Never, ever rely on it (unless otherwise documented as safe by your specific toolchain). – Lightness Races in Orbit Sep 20 '19 at 10:17

2 Answers2

2

Here's a const propagating reference_wrapper, based on cppreference's possible implementation

#include <utility>
#include <functional>
#include <type_traits>

namespace detail {
template <class T> T& FUN(T& t) noexcept { return t; }
template <class T> void FUN(T&&) = delete;
}

template <class T>
class reference_wrapper {
public:
  // types
  typedef T type;

  // construct/copy/destroy
  template <class U, class = decltype(
    detail::FUN<T>(std::declval<U>()),
    std::enable_if_t<!std::is_same_v<reference_wrapper, std::remove_cvref_t<U>> && !std::is_same_v<reference_wrapper<const T>, std::remove_cvref_t<U>>>()
  )>
  reference_wrapper(U&& u) noexcept(noexcept(detail::FUN<T>(std::forward<U>(u))))
    : _ptr(std::addressof(detail::FUN<T>(std::forward<U>(u)))) {}
  reference_wrapper(reference_wrapper&) noexcept = default;
  reference_wrapper(reference_wrapper&&) noexcept = default;

  // assignment
  reference_wrapper& operator=(reference_wrapper& x) noexcept = default;
  reference_wrapper& operator=(reference_wrapper&& x) noexcept = default;

  // access
  operator T& () noexcept { return *_ptr; }
  T& get() noexcept { return *_ptr; }
  operator const T& () const noexcept { return *_ptr; }
  const T& get() const noexcept { return *_ptr; }

  template< class... ArgTypes >
  std::invoke_result_t<T&, ArgTypes...>
    operator() ( ArgTypes&&... args ) {
    return std::invoke(get(), std::forward<ArgTypes>(args)...);
  }

  template< class... ArgTypes >
  std::invoke_result_t<const T&, ArgTypes...>
    operator() ( ArgTypes&&... args ) const {
    return std::invoke(get(), std::forward<ArgTypes>(args)...);
  }

private:
  T* _ptr;
};

template <class T>
class reference_wrapper<const T> {
public:
  // types
  typedef const T type;

  // construct/copy/destroy
  template <class U, class = decltype(
    detail::FUN<const T>(std::declval<U>()),
    std::enable_if_t<!std::is_same_v<reference_wrapper, std::remove_cvref_t<U>> && !std::is_same_v<reference_wrapper<T>, std::remove_cvref_t<U>>>()
  )>
  reference_wrapper(U&& u) noexcept(noexcept(detail::FUN<const T>(std::forward<U>(u))))
    : _ptr(std::addressof(detail::FUN<const T>(std::forward<U>(u)))) {}
  reference_wrapper(const reference_wrapper<T>& o) noexcept 
    : _ptr(std::addressof(o.get())) {}
  reference_wrapper(const reference_wrapper&) noexcept = default;
  reference_wrapper(reference_wrapper&&) noexcept = default;

  // assignment
  reference_wrapper& operator=(const reference_wrapper& x) noexcept = default;
  reference_wrapper& operator=(reference_wrapper&& x) noexcept = default;

  // access
  operator const T& () const noexcept { return *_ptr; }
  const T& get() const noexcept { return *_ptr; }

  template< class... ArgTypes >
  std::invoke_result_t<const T&, ArgTypes...>
    operator() ( ArgTypes&&... args ) const {
    return std::invoke(get(), std::forward<ArgTypes>(args)...);
  }

private:
  const T* _ptr;
};

// deduction guides
template<class T>
reference_wrapper(T&) -> reference_wrapper<T>;

You can then add const qualified access via span.

class A
{
public:
    void addData(Data& _data) {data.emplace_back(_data);}
    std::span<reference_wrapper<Data>> getData() { return { data.data(), data.size() }; }
    std::span<const reference_wrapper<Data>> getData() const { return { data.data(), data.size() }; }
private:
    std::vector<reference_wrapper<Data>> data;
}

Note that you can't copy or move the const reference_wrapper<Data>s from the second getData, and there is only access to const Data &.

Caleth
  • 52,200
  • 2
  • 44
  • 75
  • It isn't quite clear how it would help. OP wants to return a const reference to a vector in all cases. One needs a vector wrapper that propagates constness (or non-constness!) to vector *elements*, regardless of constness of the vector itself. – n. m. could be an AI Sep 20 '19 at 07:56
  • @n.m. I didn't notice it was `const vector &`, thanks. `span` is a reasonable substitute for `vector` in that case – Caleth Sep 20 '19 at 08:20
  • Your second getData returns a reference_wrapper of non-const Data. I need the data to be const. – Jakub Zaverka Sep 30 '19 at 15:02
  • @JakubZaverka I've fixed the issue of copying the `const reference_wrapper`, now there is only `const` access through `getData() const` – Caleth Sep 30 '19 at 15:23
1

Consider the visitor pattern :

struct ConstVisitor {
  virtual ~ConstVisitor() = default;
  virtual bool visit(const Data & data) = 0;//returns true if search should keep going on
};

void A::accept(ConstVisitor & visitor) const;

This way it does not matter to the outside world what kind of container Data is stored in (here a std::vector). The visitor pattern is very similar to an Enumerator in C#.

Arne J
  • 415
  • 2
  • 9