Иметь ссылку на rvalue вместо ссылки на пересылку с вариативным шаблоном

У меня есть структура X и функция foo, которая должна получать ссылки rvalue на X.

Сначала я начал с одного аргумента, и он был прост (ох... времена попроще):

auto foo(X&& arg) -> void {...};

X x;
foo(x);            // compile error [OK]
foo(std::move(x)); // accepted      [OK]
foo(X{});          // accepted      [OK]

Но затем я хотел расширить и принять переменное количество аргументов X (все еще только ссылки rvalue).

Но есть проблема.

  • Во-первых, у вас не может быть auto foo(X&&... args), что было бы идеально
  • 2. Теперь вы вынуждены делать template <class... Args> auto foo(Args&&... args), но теперь вы получаете ссылки для пересылки, которые с радостью примут невременные:
template <class... Args>
auto foo(Args&&... args) -> void { ... };

X x1, x2;
foo(x1, x2);                       // accepted [NOT OK]
foo(std::move(x1), std::move(x2)); // accepted [OK]
foo(X{}, X{});                     // accepted [OK]

Почему они использовали этот синтаксис и правила для пересылки ссылок, я с самого начала сбивался с толку. Это одна проблема. Другая проблема с этим синтаксисом заключается в том, что T&& и X<T>&& — совершенно разные звери. Но здесь мы сбились с пути.

Я знаю, как решить эту проблему с помощью static_assert или SFINAE, но оба эти решения немного усложняют ситуацию, и, по моему скромному мнению, они никогда не должны были понадобиться, если бы язык был разработан правильно для одного раза. И даже не заводи меня насчет std::initializer_list... мы снова сбились с пути.

Итак, мой вопрос: есть ли простое решение/трюк, которого мне не хватает, потому что Args&&/args рассматриваются как ссылки на rvalue?


Пока я заканчивал этот вопрос, я думал, что у меня есть решение.

Добавьте удаленные перегрузки для ссылок lvalue:

template <class... Args>
auto foo(const Args&... args) = delete;

template <class... Args>
auto foo(Args&... args) = delete;

Просто, элегантно, должно работать, давайте проверим:

X x1, x2;

foo(x1, x2);                       // compile error [OK]
foo(std::move(x1), std::move(x2)); // accepted [OK]
foo(X{}, X{});                     // accepted [OK]

Окей, у меня есть!

foo(std::move(x1), x2);            // accepted [oh c'mon]

person bolov    schedule 04.08.2017    source источник
comment
Я, вероятно, в конечном итоге использую SFINAE или static_assert, поскольку, хотя я надеюсь, что есть лучшее решение, я сомневаюсь в этом. Я просто изучаю возможности языка. Если есть дубликат, извиняюсь, не нашел.   -  person bolov    schedule 04.08.2017
comment
Вы спрашиваете решение, а затем дисквалифицироваете 2 наиболее очевидных решения (static_assert или SFINAE). Почему?   -  person Justin    schedule 04.08.2017
comment
Полностью согласен с @Justin. Пост больше похож на разглагольствование, чем на реальный вопрос. Второе решение будет проверять только первый аргумент.   -  person SergeyA    schedule 04.08.2017
comment
@ Джастин, потому что у меня нет дедлайна, я просто работаю в своем собственном темпе. В таком случае я могу позволить себе искать лучшее решение и тестировать языковые возможности, прежде чем переходить к обходному пути.   -  person bolov    schedule 04.08.2017
comment
Что касается того, почему ваша последняя идея не работает: вы звоните foo(std::move(x1), x2). Значение r для первого аргумента предотвращает выбор любой из перегрузок только для ссылки, но аргумент шаблона для второго аргумента все еще может быть выведен как ссылка на lvalue.   -  person Justin    schedule 04.08.2017


Ответы (3)


Можно получить кучу ссылок на rvalue с помощью SFINAE:

template <class... Args,
    std::enable_if_t<(!std::is_lvalue_reference<Args>::value && ...), int> = 0>
void foo(Args&&... args) { ... }

Fold-выражения — это С++ 17, достаточно просто написать метафункцию, чтобы получить такое же поведение в С++ 14. На самом деле это ваш единственный вариант - вы хотите, чтобы шаблон ограниченной функции выводил ссылки на rvalue, но единственный доступный синтаксис перегружен, чтобы означать переадресацию ссылок. Мы могли бы сделать параметры шаблона невыводимыми, но тогда вам пришлось бы их предоставлять, что кажется совсем не решением.

С концепциями это, конечно, чище, но мы на самом деле не меняем основной механизм:

template <class... Args>
    requires (!std::is_lvalue_reference<Args>::value && ...)
void foo(Args&&... args) { ... }

или лучше:

template <class T>
concept NonReference = !std::is_lvalue_reference<T>::value;

template <NonReference... Args>
void foo(Args&&... ) { ... }

Стоит отметить, что ни один из них не работает:

template <class... Args> auto foo(const Args&... args) = delete;
template <class... Args> auto foo(Args&... args) = delete;

потому что они удаляют только те перегрузки, которые принимают все ссылки lvalue, а вы хотите удалить перегрузки, которые принимают любые ссылки lvalue.

person Barry    schedule 04.08.2017
comment
вот чего я боялся. Я бы выбрал std::is_rvalue_reference<Args&&>::value (а еще лучше std::is_same<Args&&, X&&>::value). Есть ли причина, по которой вы выбрали !std::is_lvalue_reference<Args> ? - person bolov; 04.08.2017
comment
@bolov Все они означают одно и то же, я просто выбрал тот, который имел для меня наибольший смысл, поскольку Args приведет либо к некоторому X, либо к некоторому Y&, а последнее нам не нужно. Вы также можете просто сделать !std::is_reference<Args>, если мы составляем список. - person Barry; 04.08.2017
comment
@bolov Ну, если вы специально хотели X&&, то вам следует использовать его, поскольку это ограничение является наиболее конкретным. - person Barry; 04.08.2017
comment
Это ваш единственный вариант. мои рассуждения неверны или на самом деле существует другой вариант? Я не вижу недостатков в другом решении, но вы более опытны, чем я, и, возможно, я упускаю что-то очевидное. - person skypjack; 04.08.2017
comment
@skypjack Вы делаете то же самое - ограничиваете все аргументы ссылками на rvalue, только более окольным путем. - person Barry; 04.08.2017
comment
@ Барри Ну, не совсем так. В конце концов, я не использую их версию const. Вместо этого вы можете использовать тип original внутри foo. Так что, собственно, не надо удалять константы из любого типа с риском получить UB. Другими словами, я просто тестирую их с трюком Джастина, после чего в моем распоряжении оригинальный пакет. Я ошибаюсь? - person skypjack; 04.08.2017
comment
@skypjack А? То, что вы делаете, ограничивает foo(), гарантируя, что test() является допустимым вызовом - test() является допустимым вызовом, если пересылка всего в args дает rvalue - переадресация всего в args дает rvalue, если ни один из Args не является ссылочным типом lvalue. - person Barry; 05.08.2017
comment
@Barry О, извини, неправильно понял твой комментарий. Вы имели в виду, что я сделал то же самое, что и вы с std::is_lvalue_reference. Хорошо, что тогда это работает. Спасибо. ;-) - person skypjack; 05.08.2017

Я знаю, как решить эту проблему с помощью static_assert или SFINAE, но оба эти решения немного усложняют [...] есть ли простое решение/трюк, который мне не хватает [...]?

Существует довольно хороший способ сделать это без сложных выражений SFINAE или static_assert. Это также не требовало от вас угадывать, какие параметры были константными, а какие нет (это могло быстро привести вас к UB, если вы все-таки попытаетесь поиграть с константностью переменных). Кроме того, вам не нужно включать <type_traits> для этого, если вам это небезразлично.
Он основан на @Justin answer почти так и было.

Давайте посмотрим код. Просто используйте decltype и тестовую функцию, для которой вам требуется только объявление, без определения вообще:

#include<utility>

template<typename... T>
void test(const T &&...);

template <class... Args>
auto foo(Args&&... args)
-> decltype(test(std::forward<Args>(args)...), void())
{}

struct X {};

int main() {
    X x1, x2;
    //foo(x1, x2);
    foo(std::move(x1), std::move(x2));
    foo(X{}, X{});
}

Если вы переключите комментарий, пример больше не будет компилироваться в соответствии с запросом.

Основная идея почти такая же, как обсуждалось в другом ответе: если это ссылка на rvalue, вы можете назначить ее ссылке const rvalue. В любом случае, поскольку вы не хотите делать константными те параметры, которые изначально не были константными, просто протестируйте их все вместе, а затем используйте исходные.

person skypjack    schedule 04.08.2017
comment
Мне очень нравится этот трюк. Функция test очень многоразовая. Вы можете сделать функцию пригодной для использования только в неопределенном контексте, подобно std::declval (я бы, вероятно, просто поставил = delete, чтобы было более понятно, что функция не предназначена для использования в оцениваемом контексте). - person Justin; 06.08.2017
comment
@skypjack = delete не исключает его из набора перегрузок IIRC. Функция выбирается, затем компилятор говорит, что она удалена, ее нельзя использовать. - person Justin; 06.08.2017
comment
@Justin Достаточно честно. Ты прав. Я неправильно использовал термины, но я надеюсь, что вы получите сообщение. В любом случае результат тот же, вы не можете его использовать в неопределенном контексте. Так что мой пример не компилируется, если я его = delete. - person skypjack; 06.08.2017
comment
@skypjack Имеет смысл. Интересно, как declval это делает; они могут просто оставить его нереализованным или использовать поддержку компилятора - person Justin; 06.08.2017
comment
@Justin Это определяется языком, поэтому я бы сказал, что все дело в деталях реализации. Я не думаю, что это когда-либо может быть нереализованной функцией. - person skypjack; 06.08.2017

Если вы действительно хотите избежать SFINAE, вы можете принять свои аргументы const&&; они не будут универсальными ссылками:

template <class... Args>
auto foo(const Args&&... args) -> void { ... };

const&& может быть привязан только к rvalue, но не к lvalue.

#include <iostream>

template <class... Args>
auto foo(const Args&&... args) -> void
{
    std::cout << __PRETTY_FUNCTION__ << '\n';
}

struct X {};

int main () {
    X x1, x2;
    // foo(x1, x2);                    // compile error
    foo(std::move(x1), std::move(x2)); // accepted [OK]
    // foo(std::move(x1), x2);         // compile error
    foo(X{}, X{});                     // accepted [OK]
}

На Coliru

К сожалению, тогда вам придется отбросить const, так что это не самое красивое решение.

person Justin    schedule 04.08.2017
comment
Вам пришлось бы отбросить const... и тогда, как бы вы узнали, какие rvalue на самом деле не были const (сделав это безопасным), а какие были (сделав это UB)? - person Barry; 04.08.2017
comment
@Barry Да, я бы определенно рекомендовал не делать этого таким образом. Но я не могу придумать пример в обычном коде, где у вас был бы параметр функции const&&. Единственный способ, который я могу придумать, - это заставить функцию возвращать const&&, чего я не вижу в обычном коде. - person Justin; 04.08.2017
comment
@Downvoter Почему ты понизил это? Что я могу сделать, чтобы улучшить этот ответ или будущие ответы? - person Justin; 04.08.2017