Jakub Zaverka
Jakub Zaverka

Reputation: 8874

Const correctness of STL library with reference-wrapper?

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.

Upvotes: 0

Views: 453

Answers (2)

Caleth
Caleth

Reputation: 62864

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 &.

Upvotes: 2

Arne J
Arne J

Reputation: 415

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#.

Upvotes: 1

Related Questions