Перенаправление того же значения в две или более функции

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

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, функция работает просто отлично. Тем не менее, я беспокоюсь о ситуациях, когда rvalues ​​передаются ему, потому что значение становится недействительным, как только происходит операция перемещения. Мое сомнение заключается в следующем: есть ли правильный способ пересылки контейнера в этом случае без потери категории значений?

Ответ 1

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

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

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

Ответ 2

На самом деле нет версии raleue-reference версии std::begin - мы просто имеем (отбрасываем 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);
}

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

Ответ 3

Вы, по-видимому, понимаете, что не хотите 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)

Ответ 4

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

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

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

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