Лучший способ вернуться раньше из функции, возвращающей ссылку

Допустим у нас есть функция вида:

const SomeObject& SomeScope::ReturnOurObject()
{
    if( ! SomeCondition )
    {
        // return early
        return  ;
    }

    return ourObject;
}

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

14 ответов


это не синтаксическая проблема, а проблема дизайна. Вы должны указать, что ReturnOurObject() должен вернуться, когда SomeCondition - Это правда. Это зависит главным образом от того, для чего будет использоваться функция. И что Вы нам не сказали.

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

  • верните ссылку на какой-то другой объект; у вас должен быть какой-то эрзац-объект где-то
  • есть специальные объект "no-object-to-return", на который вы возвращаете ссылку; клиенты могут проверить это; если они не проверяют, они получают разумное поведение по умолчанию
  • возвращает указатель, а не ссылку; клиенты должны всегда проверять возвращаемое значение функции
  • бросать исключение, если SomeCondition является чем-то исключительным, что клиенты не могут иметь дело с тем, что было бы уместно
  • утверждать; если SomeCondition должно всегда держать, оно должно быть утверждал

в этом случае я бы использовал указатель вместо ссылки. На самом деле этот критерий (необязательное или обязательное возвращаемое значение) - это то, как я решаю между указателями и ссылками в первую очередь.


Я бы сделал так:

const SomeObject& SomeScope::ReturnOurObject()
{
    if( ! SomeCondition )
    {
        throw SomeException();
    }
    return ourObject;
}

const SomeObject *SomeScope::ReturnOurObjectIfPermitted()
{
    return SomeCondition ? &ourObject : 0;
}

и, возможно, также:

bool SomeScope::CheckMode();
    return SomeCondition;
}

у абонентов есть несколько вариантов:

// 1 - "I know that I'm in the right mode"
myScope.ReturnOurObject().DoSomething();

// 2 - "I don't know whether I'm in the right mode, but I can cope either way"
if (SomeObject *myObject = myScope.ReturnOurObjectIfPermitted()) {
    myObject->DoSomething();
} else {
    DoSomethingElse();
}

// 2 - alternate version:
if (myScope.CheckMode()) {
    SomeObject &myObject = myScope.ReturnOurObject();
    myObject.DoSomething();
} else {
    DoSomethingElse();
}

// 3 - "I don't know whether I'm in the right mode. If I'm not then
// I can't deal with it now, but some higher-level code can"
try {
    // ... several calls deep ...
    myScope.ReturnOurObject().DoSomething();
    // ... several returns back ...
} catch (SomeException &e) {
    DoSomethingElse();
}

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

если это ответственность вызывающего абонента, то им нужна способность обеспечить это. Ты все равно захочешь проверить, просто на всякий случай. Исключение при так называемые" предварительные условия " не выполняются, что делает их более похожими на советы, чем условия, и это может работать довольно хорошо, если вы не возражаете против накладных расходов во время выполнения проверок повсюду. Я обычно довольно неумолим с предварительными условиями, поэтому на самом деле я мог бы заменить условное исключение утверждением и документом, что функция не должна вызываться в неправильном режиме. Это зависит от того, сколько контроля вызывающий имеет над режимом-очевидно, если он изменяется произвольно (например, если" SomeCondition "на UART нет байта"), тогда вам нужен совсем другой интерфейс, чем если бы он только когда-либо менялся, когда клиентский код вызывает некоторую функцию для его изменения.

если это не ответственность вызывающего абонента, чтобы получить правильный режим, то ваш интерфейс не должен писать чеки, которые ваша реализация не может обналичить. В этом случае, если "ожидается", что не будет объекта для возврата, то возврат не должен быть по ссылке. Если только (a) вы не можете шуршать специальным "извините, это не сработало "объект для возврата, который вызывающий может проверить, или (б) вам удобно бросать исключения в" ожидаемых " ситуациях. ИМО (b) редко встречается на C++. (a) обычно не лучше, чем возврат нулевого указателя, но иногда это так. Например, возврат пустой коллекции означает, что "здесь вам ничего не разрешено видеть" может привести к очень чистому вызывающему коду, если DoSomethingElse() было бы "ничего не делать". Так же как и желаемые обязанности, интерфейс зависит от ожидаемых вариантов использования.


Если вы не хотите изменять тип возврата на указатель, вы можете использовать null шаблон объекта


каков срок службы "ourObject" и где он создается?

Если вам нужно вернуться раньше, не касаясь / обновления ourObject, и он существует в точке, куда вы возвращаетесь, я не вижу проблемы с возвратом ссылки на него.

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

Если вы возвращаете ссылку на временный, то лучше исправить это вопрос...


Я бы вернул логическое значение и передал ссылку на объект в качестве параметра функции:

bool getObject(SomeObject& object)
{
    if( condition )
         return false;

    object = ourObject;
    return true;
}

в этом случае теперь ваш объект был заполнен только в том случае, если функция возвращает true.


эээ... просто сделайте исключение...


исключение


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

  • Если копирование объекта не дорого, верните копию
  • Если копировать дорого, верните константный указатель. Возвращает NULL в случае неудача.

Если вы не хотите создавать исключение, вы можете попробовать что-то вроде:

const SomeObject& GetSomeObject(const SomeObject& default_if_not_found = SomeObject())
{
    // ...
    if (!found)
        return default_if_not_found;

    return ourObject;
}

это означает, что вам не нужно иметь какую-то статическую переменную, лежащую где-то, чтобы вернуться, и вы все еще можете вызвать ее без параметров. Я предполагаю, что состояние SomeObject (), построенное по умолчанию, является пустым состоянием, поэтому вы можете проверить SomeObject, чтобы увидеть, находится ли он в пустом состоянии. Также иногда полезно использовать это для переопределения возвращаемого значения по умолчанию, например. GetSomeObject (SomeObject ("я пуст"))


ровно три варианта...

1. исключение
2. возврат фиктивного объекта ошибки
3. используйте указатель и возвращайте NULL


Я считаю, что ответ зависит от того, являетесь ли вы программистом, который считает исключения плохими и никогда не хочет их использовать или нет. Если вы используете их, и это действительно исключительное условие, то должно быть создано исключение. Да, пользователям нужно разобраться с этим, но скрывать ошибку, чтобы притвориться, что ничего не произошло, просто не вариант. Но если вы никогда не используете исключения, просто верните указатель или логическое значение и добавьте ссылку non-const на SomeObject к вашему функция. Оба решения просты, но кто сказал, что чрезмерное усложнение делает код лучше?


способ настройки функции, вы должны вернуть SomeObject или бросить исключение. Если вы хотите сделать что-то другое, нужно изменить функцию. Другого выбора нет.

если SomeCondition никогда не будет false, тогда вы можете удалить тест в коде и поставить assert вместо этого, или вы можете сохранить тест и создать исключение. (Я не хочу рекомендовать просто игнорировать возможность, так как "никогда не ложно" почти всегда означает " никогда не ложно, если что-то плохое произошло и не было обнаружено", и если это стоит проверить при любых условиях, это стоит обнаружить.)

существует возможность возврата какого-то нулевого объекта. Указатели полезны для этого, так как есть очевидное значение null, которое проверяет false, но вы можете передать флаг обратно как параметр out или вернуть специальный объект, предназначенный как null. Затем вам нужно будет проверить возвращаемое значение для каждого вызова, чтобы это было значимым, и это вряд ли будет меньше хлопот, чем assert или throw.

если бы я это делал, это зависело бы от SomeCondition. Если он имеет побочные эффекты или не трудно рассчитать, протестировать и выдать исключение, если false. Если это обременительно для расчета, используйте assert Так что вам не нужно делать это в производстве, и забыть об этом, кроме этого.


это та же ситуация, что и встроенный оператор C++dynamic_cast<>. Он может привести к типу указателя или ссылочному типу. Когда это тип указателя, он возвращает NULL на провал. Когда это ссылочный тип, он выдает исключение std::bad_cast на провал. Следуя тому же примеру, вы создадите исключение.