При использовании ссылок пересылки - плохая идея пересылать одно и то же значение более чем одной функции? Рассмотрим следующий фрагмент кода:

template<typename Container>
constexpr auto
front(Container&& c)
-> typename Container::value_type
{ return std::forward<Container>(c).front(); }

template<typename Container>
constexpr auto
back(Container&& c)
-> typename Container::value_type
{ return std::forward<Container>(c).back(); }

template<typename Container>
constexpr auto
get_corner(Container&& c)
{
    return do_something(front(std::forward<Container(c)),
                        back(std::forward<Container>(c));
}

Если Container является lvalue-ссылкой, функция работает нормально. Однако меня беспокоят ситуации, когда ему передаются rvalue, потому что это значение станет недействительным после того, как произойдет перемещение. Я сомневаюсь: есть ли в этом случае правильный способ пересылки контейнера без потери категории ценности?

12
Mário Feroldi 5 Сен 2016 в 01:44

4 ответа

Лучший ответ

В общем, для одной и той же функции нецелесообразно передавать один и тот же параметр дважды. Нет, если у него нет конкретных знаний о том, что будет делать получатель этого перенаправленного параметра.

Помните: поведение std::forward может быть эквивалентно поведению std::move, в зависимости от того, какой параметр передал пользователь. И поведение значения x будет зависеть от того, как его обрабатывает принимающая функция. Если получатель принимает неконстантную ссылку на rvalue, он, скорее всего, переместится из этого значения, если это возможно. Это оставило бы вас держать перемещенный объект. Если он принимает значение, он обязательно перемещается от него, если тип его поддерживает.

Поэтому, если у вас нет конкретных знаний об ожидаемом поведении используемых вами операций, небезопасно пересылать параметр более одного раза.

14
Nicol Bolas 5 Сен 2016 в 00:20

В общем да, это потенциально опасно.

Пересылка параметра гарантирует, что если значение, полученное параметром универсальной ссылки, является каким-то rvalue, оно будет продолжать оставаться rvalue при пересылке. Если значение в конечном итоге передается функции (например, конструктору перемещения), которая потребляет значение, перемещаясь от него, его внутреннее состояние вряд ли будет действительным для использования в последующих вызовах.

Если вы не передадите параметр, он (как правило) не будет допущен к операциям перемещения, поэтому вы будете защищены от такого поведения.

В вашем случае front и back (как свободные функции, так и функции-члены) не выполняют перемещения контейнера, поэтому приведенный вами конкретный пример должен быть безопасным. Однако это также демонстрирует, что нет причин пересылать контейнер, так как rvalue не будет обрабатываться иначе, чем lvalue - это единственная причина сохранить различие, перенаправив значение в первую очередь.

2
Kyle Strand 5 Сен 2016 в 00:33

На самом деле нет версии std::begin со ссылкой на rvalue - у нас просто есть (не считая constexpr и возвращаемых значений):

template <class C>
??? begin(C& );

template <class C>
??? begin(C const& );

Для контейнеров lvalue вы получите iterator, а для контейнеров rvalue вы получите const_iterator (или как бы там ни было, в конечном итоге будет эквивалент для конкретного контейнера).

Единственная реальная проблема в вашем коде - это возврат decltype(auto). Для контейнеров lvalue это нормально - вы вернете ссылку на объект, время жизни которого превышает функцию. Но для контейнеров rvalue это возвращает висящую ссылку. Вы захотите вернуть ссылку для контейнеров lvalue и значение для контейнеров rvalue.

Кроме того, forward - вставлять контейнеры в begin() / end(), вероятно, не то, что вы хотите. Было бы более эффективно условно обернуть результат select() как итератор перемещения. Что-то вроде этого моего ответа:

template <typename Container,
          typename V = decltype(*std::begin(std::declval<Container&>())),
          typename R = std::conditional_t<
              std::is_lvalue_reference<Container>::value,
              V,
              std::remove_reference_t<V>
              >
          >
constexpr R operator()(Container&& c)
{
    auto it = select(std::begin(c), std::end(c));
    return *make_forward_iterator<Container>(it);
}

Вероятно, есть менее подробный способ выразить все это.

4
Community 23 Май 2017 в 12:10

Вы, вероятно, понимаете, что не хотите std::move передавать объект нескольким функциям:

std::string s = "hello";
std::string hello1 = std::move(s);
std::string hello2 = std::move(s);  // hello2 != "hello"

Роль forward состоит в том, чтобы просто восстановить любой статус rvalue, который имел параметр, когда он был передан функции.

Мы можем быстро продемонстрировать, что это плохая практика, forward добавив один параметр два раза в функцию, которая имеет эффект перемещения:

#include <iostream>
#include <string>

struct S {
    std::string name_ = "defaulted";
    S() = default;
    S(const char* name) : name_(name) {}
    S(S&& rhs) { std::swap(name_, rhs.name_); name_ += " moved"; }
};

void fn(S s)
{
    std::cout << "fn(" << s.name_ << ")\n";
}

template<typename T>
void fwd_test(T&& t)
{
    fn(std::forward<T>(t));
    fn(std::forward<T>(t));
}

int main() {
    fwd_test(S("source"));
}

http://ideone.com/NRM8Ph

Если пересылка была безопасной, мы должны увидеть fn(source moved) дважды, но вместо этого мы увидим:

fn(source moved)
fn(defaulted moved)
3
kfsone 5 Сен 2016 в 00:34