Функция, принимающая как lvalue, так и rvalue аргументы
Есть ли способ написать функцию на C++, которая принимает как lvalue, так и rvalue аргументы, не используя шаблоны?
Например, предположим, что я пишу функцию print_stream
, которая считывает данные из istream
и выводит их на экран или что-то в этом роде.
Я считаю вполне разумным вызывать print_stream
так:
fstream file{"filename"};
print_stream(file);
а также так:
print_stream(fstream{"filename"});
Но как мне объявить print_stream
, чтобы оба варианта вызова работали?
Если я объявлю её так:
void print_stream(istream& is);
то второй вариант не скомпилируется, поскольку rvalue не может привязываться к неконстантной lvalue ссылке.
Если я объявлю её так:
void print_stream(istream&& is);
то первый вариант не скомпилируется, так как lvalue не может привязываться к rvalue ссылке.
Если я объявлю её так:
void print_stream(const istream& is);
то реализация функции не скомпилируется, потому что нельзя читать из const istream
.
Я не могу сделать функцию шаблоном и использовать "универсальную ссылку", потому что реализация должна быть отдельно скомпилирована.
Я мог бы предоставить две перегрузки:
void print_stream(istream& is);
void print_stream(istream&& is);
и оставить вторую перегрузку, вызывающую первую, но это кажется излишним, и мне бы не хотелось делать так всякий раз, когда я пишу функцию с подобной семантикой.
Существует ли лучший подход к решению этой проблемы?
5 ответ(ов)
В данном случае не так много разумных вариантов, кроме как предложить два перегруженных варианта функции или сделать её шаблонной.
Если вам действительно, действительно нужен (уродливый) альтернативный подход, то, пожалуй, единственный (безумный) вариант — это сделать так, чтобы функция принимала const&
, с предварительным условием, что вы не можете передавать объект с const
-квалификацией (вы всё равно не хотите поддерживать это). Функция тогда сможет «сбросить» квалификатор const
у ссылки.
Тем не менее, я бы лично предпочёл написать две перегрузки и определить одну из них через другую, так вы будете дублировать только декларацию, но не определение:
void foo(X& x)
{
// Здесь будет код...
}
void foo(X&& x) { foo(x); }
Ваша реализация функции print
, использующая универсальную ссылку и std::enable_if
, выглядит вполне корректно. Давайте рассмотрим её подробнее:
// Из-за универсальной ссылки
// Шаблонная функция с && может обрабатывать как rvalue, так и lvalue
// Мы можем использовать std::is_same, чтобы ограничить T, чтобы он обязательно был istream
// Это альтернативный подход, и, на мой взгляд, он лучше, чем две перегруженные функции
template <typename T>
typename std::enable_if<
std::is_same<typename std::decay<T>::type, istream>::value
>::type
print(T&& t) {
// Вы можете получить реальный тип значения, используя forward
// std::forward<T>(t)
}
Объясню, как это работает. Использование std::enable_if
в сочетании с std::is_same
позволяет ограничить тип T
так, чтобы он мог принимать только объекты типа istream
. Это действительно сокращает необходимость в перегрузках функций, так как в вашем коде уже будет проверка типов на этапе компиляции.
Такой подход полезен, когда нужно работать с конкретными типами и избегать неявных ошибок, которые могут возникнуть из-за смешения типов.
Если у вас есть дополнительные вопросы о том, как улучшить или адаптировать данный шаблон для других типов, пожалуйста, дайте знать!
Вот решение, которое может обрабатывать любое количество параметров и не требует, чтобы принимающая функция была шаблоном.
#include <utility>
template <typename Ref>
struct lvalue_or_rvalue {
Ref &&ref;
template <typename Arg>
constexpr lvalue_or_rvalue(Arg &&arg) noexcept
: ref(std::move(arg))
{ }
constexpr operator Ref& () const & noexcept { return ref; }
constexpr operator Ref&& () const && noexcept { return std::move(ref); }
constexpr Ref& operator*() const noexcept { return ref; }
constexpr Ref* operator->() const noexcept { return &ref; }
};
#include <fstream>
#include <iostream>
using namespace std;
void print_stream(lvalue_or_rvalue<istream> is) {
cout << is->rdbuf();
}
int main() {
ifstream file("filename");
print_stream(file); // вызов с lvalue
print_stream(ifstream("filename")); // вызов с rvalue
return 0;
}
Я предпочитаю это решение среди остальных, потому что оно идиоматично, не требует написания шаблона функции каждый раз, когда вы хотите его использовать, и генерирует разумные ошибки компилятора, например...
print_stream("filename"); // упс! забыл создать ifstream
test.cpp: In instantiation of 'constexpr lvalue_or_rvalue<Ref>::lvalue_or_rvalue(Arg&&) [with Arg = const char (&)[9]; Ref = std::basic_istream<char>]':
test.cpp:33:25: required from here
test.cpp:10:23: error: invalid initialization of reference of type 'std::basic_istream<char>&&' from expression of type 'std::remove_reference<const char (&)[9]>::type' {aka 'const char [9]'}
10 | : ref(std::move(arg))
| ^
Изюминкой этого решения является то, что оно также поддерживает неявное применение конструкторов преобразования и операторов преобразования, определенных пользователем…
#include <cmath>
struct IntWrapper {
int value;
constexpr IntWrapper(int value) noexcept : value(value) { }
};
struct DoubleWrapper {
double value;
constexpr DoubleWrapper(double value) noexcept : value(value) { }
};
struct LongWrapper {
long value;
constexpr LongWrapper(long value) noexcept : value(value) { }
constexpr LongWrapper(const IntWrapper &iw) noexcept : value(iw.value) { }
constexpr operator DoubleWrapper () const noexcept { return value; }
};
static void square(lvalue_or_rvalue<IntWrapper> iw) {
iw->value *= iw->value;
}
static void cube(lvalue_or_rvalue<LongWrapper> lw) {
lw->value *= lw->value * lw->value;
}
static void square_root(lvalue_or_rvalue<DoubleWrapper> dw) {
dw->value = std::sqrt(dw->value);
}
void examples() {
// неявное преобразование от int к IntWrapper&& через конструктор
square(42);
// неявное преобразование от IntWrapper& к LongWrapper&& через конструктор
IntWrapper iw(42);
cube(iw);
// неявное преобразование от IntWrapper&& к LongWrapper&& через конструктор
cube(IntWrapper(42));
// неявное преобразование от LongWrapper& к DoubleWrapper&& через оператор
LongWrapper lw(42);
square_root(lw);
// неявное преобразование от LongWrapper&& к DoubleWrapper&& через оператор
square_root(LongWrapper(42));
}
Таким образом, это решение предоставляет гибкий и эффективный способ работы с различными типами, а также благоприятные сообщения об ошибках.
Да, еще один довольно неэстетичный вариант — сделать функцию шаблоном и явно инстанцировать обе версии:
template<typename T>
void print(T&&) { /* ... */ }
template void print<istream&>(istream&);
template void print<istream&&>(istream&&);
Это может быть скомпилировано отдельно. Клиентскому коду нужна только декларация шаблона.
Лично я бы предпочел следовать тому, что предлагает Эндри Прал, хотя бы из-за лучшей читаемости и простоты.
Будьте смелыми, используйте обобщенные функции с параметрами и давайте им подходящие имена.
template<typename Stream>
auto stream_meh_to(Stream&& s)
->decltype(std::forward<Stream>(s) << std::string{/* */}) {
return std::forward<Stream>(s) << std::string{"meh\n"};
}
Обратите внимание, что эта функция будет работать с любым типом, который имеет смысл использовать в данном контексте, а не только с ostream
. Это является хорошим качеством.
Если функция будет вызвана с аргументом, который не имеет смысла, она просто проигнорирует это определение. Кстати, лучше всего это работает при установке отступов на 4 пробела. 😃
Это тот же подход, что и у Cube, но я утверждаю, что, когда это возможно, более элегантно не проверять конкретные типы и позволить обобщенному программированию делать свое дело.
Что означает T&& (двойной амперсанд) в C++11?
Какова разница между 'typedef' и 'using'?
Каковы преимущества инициализации списка (с использованием фигурных скобок)?
Возможно ли вывести тип переменной в стандартном C++?
Эквивалент unique_ptr в Boost?