3

Recently, I've decided to write a class storing a variant with reference_wrapper<const vector> and vector for having a choice either to own the value or having only a reference of it. That is, std::variant<vector<string>, reference_wrapper<const vector<string>>>.

The interesting part is what the variant stores depending on initialization. I did a small investigation, and it turned out, that in all cases vector<string> type wins, except for the case when passing via std::cref. The same applies to functions (somewhat expected, because constructors are similar to functions in this way)

void f(vector<string>); // #1
void f(reference_wrapper<const vector<string>>); // #2

vector<string> data;
const vector<string>& get_data();

f(data); // #1
f(std::cref(data)) // #2

f(get_data()); // #1
f(std::cref(get_data())) // #2

The question is why the vector<string> has the priority here. I looked at Best viable function section here , but it didn't make much sense. It seems, that

4) or, if not that, F1 is a non-template function while F2 is a template specialization

part chooses vector<string> over reference_wrapper<vector<string>> (because reference_wrapper constructor is templated), but I'm not sure, because I can't fully understand if they are equal using the rule

1) There is at least one argument of F1 whose implicit conversion is better than the corresponding implicit conversion for that argument of F2

Can someone please describe all the implicit conversions applied in each case and show the true reason why one overload is preferred over another? To me, they are as follows:

f(data) = f(vector<string>&) -> (*exact match* implicit conversion) -> f(vector<string>)

f(data) = f(vector<string>&) -> (*conversion* implicit conversion) -> f(reference_wrapper<vector<string>>)

Did I miss something?

Another question, connected to this topic: Ranking of implicit conversion sequences section again,here leaves a question, is T(const T&) considered an Exact match (user-defined conversion of class type to the same class) or Conversion?

2 Answers2

1

First, Although std::reference_wrapper is part of standard library it is treated as user-defined type.

For example an implicit conversion from std::vector & to const std::vector & is always preferred over an implicit conversion from std::vector& to std::reference_wrapper<vector>. That is because (as per standard) the former one is a standard conversion, but the later is a user-defined conversion. the first one is called standard conversion because it adds a const to your type but the second is treated as converting some type to totally different type.

check this code and see cppreference.com.

Second, I'm trying to guess some good alternative. I see you want either to store a reference to vector OR move/(copy as cheap as possible) or (you could say directly initialize) the data inside your class if it is not already stored (safely) in some variable. Maybe you could consider using move semantics. you can play with code here

using TVar = std::variant<reference_wrapper<const vector<string>>, vector<string>>;

class Config {
private:
    TVar data;
public:
    const vector<string>& get_data() const{
        if (data.index() == 1)
            return get<1>(data);
        else return get<0>(data);
    }
    Config(const vector<string>& it):data(cref(it)){}
    Config(vector<string>&& it):data(move(it)){}
};

Here we have two functions.

  1. One that takes reference to "stored value" (more precisely lvalue). wrapping it in cref so that it causes the reference_wrapper alternative in the variant to be the best overload.
  2. The other does the magic. it is the reference to values that are either written directly (aka pvalues) and values that use the magic std::move function (aka xvalues). if you have never seen this, please reference this respectable Q&A What is move semantics?

catch(...) :), this is it. also notice, you don't need the std::monostate as this is only needed for making the variant default constructible (with no arguments). you can make your class default constructible like this.

    Config(vector<string>&& it = {}):data(move(it)){}
-3

There is absolutely no reason to store reference_wrapper whatsoever. Just use a pointer like any sane programmer. reference_wrapper is used to properly trigger std::invoke and associated classes/functions like thread and bind.

ALX23z
  • 4,456
  • 1
  • 11
  • 18
  • 1
    Yes, you are right, but `reference_wrapper` guarantees that there is no `nullptr` inside, which is convenient. – koleydoscope Aug 21 '21 at 21:11
  • 1
    Eh, no reason? The utilities you mention **do store it**. That's their spiel, decay copying everything into internal storage. Don't know what the OP is doing exactly, but "no reason" is simply not true. – StoryTeller - Unslander Monica Aug 21 '21 at 21:20
  • @koleydoscope lack of default constructor is IMO worse than possibility of being a null - not to mention that you already have a variant over it - meaning you already need to make checks. If you really want a "non-zero" pointer than write a class `non_zero_ptr` that accepts pointers and asserts on `nullptr`. This way you'll also have a clear distinction between when your variant gets a vector and when it gets a reference - if you give it a pointer or an instance. – ALX23z Aug 21 '21 at 21:28
  • @StoryTeller-UnslanderMonica you should try and understand what is being implied by people rather than taking it literally. Purposely misinterpreting this way isn't particularly helpful. – ALX23z Aug 21 '21 at 21:34
  • People should be explicit when answering. Otherwise, the implication is the answer isn't useful. – StoryTeller - Unslander Monica Aug 21 '21 at 21:39
  • 3
    This doesn't answer the question, which is about overload resolution rules, not whether the use of `reference_wrapper` is a good idea or not, or if alternatives exist. – cigien Aug 21 '21 at 22:05