Есть ли способ сделать передачу по ссылке и передать значение явно в вызове функции?
Если вы посмотрите на этот код,
int x = 0;
function(x);
std::cout << x << '\n';
вы не сможете проверить с помощью каких-либо средств синтаксиса, этот параметр x передается по ссылке или что он передается по значению. Единственный способ, которым вы наверняка знаете, - это посмотреть на объявление функции или определение функции.
Вот простой пример того, как я считаю, что это может быть проблемой:
std::string Lowercase(std::string str); //<- this is hidden away in code; probably in a different file.
int main(){
std::string str = "HELLO";
Lowercase(str);
std::cout << str << '\n'; //<- Bug! we expected to output "hello". The problem is not very easy to spot, especially when a function name sounds as though it will change the passed in value.
}
Чтобы избежать необходимости переходить между вызовом функции и объявлением функции (или, в некоторых случаях, документацией), чтобы понять поведение функции, есть способ явно документировать синтаксис вызова функции, который параметр, как ожидается, изменится (т.е. опорный параметр) или что копия отправляется (т.е. передается по значению)?
Я понимаю, что существует также возможность прохождения через const & который имеет аналогичное понятие о передаче по значению, в том, что переменная, переданная внутри, не будет иметь своего значения после вызова функции.
Я уверен, что на языке есть всевозможные ситуации, которые могут добавить к сложности понимания того, как передается параметр,
но мне любопытно, есть ли способ бороться с этой проблемой так, как я хочу?
Я заметил, что некоторые люди пишут две подобные функции. Один из них принимает параметр значения, другой - указатель. Это позволяет вызывать такую функцию:
Lowercase(str); //we assume the value will not change
Lowercase(&str); //we assume the value will change
Но это решение имеет много других проблем, и я не хотел бы терять пользу ссылок. Кроме того, мы по-прежнему делаем предположения о поведении.
Ответы
Ответ 1
Некоторые люди настаивают на том, что правильным способом передачи измененного объекта является использование указателя. То есть, вы пройдете
Lowercase(&str);
... и Lowercase()
, очевидно, было бы реализовано для ввода указателя. Такой подход может удовлетворить ваши потребности.
Я хочу упомянуть, однако, что это не, что бы я сделал! Вместо этого подход, который я предпочитаю, заключается в том, чтобы вместо этого использовать соответствующие имена. Например,
inplace_lowercase(str);
в значительной степени говорит, что он собирается делать. Очевидно, что inplace_lowercase()
на самом деле был бы алгоритмом и с малой магией можно было бы разумно назвать
inplace_lowercase(str.begin() + 1, str.end());
.
Вот несколько причин, по которым мне не нравится передавать аргументы указателем и/или почему я не верю в явное указание того, как передается аргумент:
- Указатели могут быть пустыми. Обязательные контрольные параметры должны, на мой взгляд, быть ссылкой.
- Передача указателем по-прежнему не указывает, может ли аргумент быть изменен, так как аргумент не может быть
T const*
.
- Наличие значимых имен позволяет на самом деле легче понять, что происходит в первую очередь.
- Вызов чего-либо без консультации с его документацией и/или зная, что вызываемая функция будет работать, не работает в любом случае и указывает, как все прошло, пытается вылечить симптомы более глубокой проблемы.
Ответ 2
Я не уверен, что полностью понимаю ваши требования, но, возможно, это то, что вы можете использовать:
template<typename T>
void foo( T ) { static_assert( sizeof(T)==0, "foo() requires a std::ref" ); }
void foo( std::reference_wrapper<int> t )
{
// modify i here via t.get() or other means of std::reference_wrapper
}
int main()
{
int i = 42;
// foo( i ); // does not compile, static_assert fires
foo( std::ref( i ) ); // explicit std::ref visible on the caller side
}
Ответ 3
Многие (большинство) IDE помогают вам справиться с этой проблемой, показывая прототип функции/метода, когда они выяснят, какую функцию вы вызываете.
Ответ 4
Это С++: отсутствие параметров in
и out
не означает, что язык недостаточен, это означает, что вам нужно реализовать то, что другие языки будут использовать в качестве языковой функции в качестве библиотеки.
Создайте два класса и функции template
.
in_param<T>
является оберткой вокруг T const&
, а io_param<T>
является оберткой вокруг ссылки T&
. Вы создаете их, вызывая вспомогательные функции in
и io
.
Внутри они ведут себя как ссылки (через перегрузку).
Снаружи вызывающий должен вызывать in
или io
в аргументе, отмечая его на сайте вызова.
out
сложнее: внутри дымки только назначение является законным. В идеале мы бы даже не построили его: может помочь метод emplace
.
Однако вызывающему абоненту нужен какой-то канал, чтобы узнать, был ли построен параметр или нет.
Что бы я сделал, это out_param
имеет только operator=
, и он присваивает. out
обертывает что-то в out_param
. Если вы хотите отложить конструкцию, используйте optional
внутри параметра out, который приближается. Может быть, out_param
также имеет emplace
, который обычно просто присваивает, но если обернутый tyoe имеет emplace
вместо этого вызывает его?
template<typename T>
struct in_param : std::reference_wrapper<T const> {
explicit in_param( T const& t ):std::reference_wrapper<T const>(t) {}
in_param( in_param<T>&& o ):std::reference_wrapper<T const>(std::move(o)) {}
void operator=( in_param<T> const& o ) = delete;
};
template<typename T>
struct io_param : std::reference_wrapper<T> {
explicit io_param( T& t ):std::reference_wrapper<T>(t) {}
io_param( io_param<T>&& o ):std::reference_wrapper<T>(std::move(o)) {}
};
template<typename T>
in_param< T > in( T const& t ) { return in_param<T>(t); }
template<typename T>
io_param< T > io( T& t ) { return io_param<T>(t); }
template<typename T>
struct out_param {
private:
T& t;
public:
out_param( T& t_ ):t(t_) {}
out_param( out_param<T>&& o ):t(o.t) {}
void operator=( out_param<T> const& o ) = delete;
void operator=( out_param<T> && o ) = delete;
void operator=( out_param<T> & o ) = delete;
void operator=( out_param<T> && o ) = delete;
template<typename U>
out_param<T>& operator=( U&& u ) {
t = std::forward<U>(u);
return *this;
}
// to improve, test if `t` has an `emplace` method. If it does not,
// instead do t = T( std::forward<Us>(us)... ). (I'd use tag dispatching
// to call one of two methods)
template<typename... Us>
void emplace( Us&&... us ) {
t.emplace( std::forward<Us>(us)... );
}
};
template<typename T>
out_param<T> out( T& t ) { return out_param<T>(t); }
или что-то вроде выше.
Теперь вы получите синтаксис как:
void do_stuff( int x, in_param<expensive> y, io_param<something> z, out_param<double> d );
int main() {
expensive a;
something b;
double d;
do_stuff( 7, in(a), io(b), out(d) );
}
и отказ вызова in
, io
или out
на сайте вызова приводит к ошибкам времени компиляции. Кроме того, out_param
затрудняет случайное считывание состояния переменной out
внутри функции, создавая очень приятную документацию на сайте вызова.
Ответ 5
Если вы используете MS VС++, то, возможно, это будет полезная информация об языке комментариев к исходному коду (SAL)
http://msdn.microsoft.com/ru-ru/library/hh916383.aspx
Ответ 6
Я думаю, что это бесполезно уведомлять (по-русски [1]). Единственный необходимый вопрос: "Является ли мой объект семантически изменен?", И так:
- Когда вы читаете прототип, вы знаете, может ли функция изменять объект (не const const) или нет (copy или const ref).
- Когда вы используете функцию (даже не прочитав прототип [2]), если вы должны быть уверены, что не изменяете объект, используйте const_cast.
[1] Статический анализатор может сделать это для своих целей.
[2] Если вы пропустите, компилятор все равно предупредит вас.
Ответ 7
В этом весь смысл передачи по ссылке - синтаксически не нужно ничего делать, кроме передачи по значению.