Перемещение семантики и виртуальных методов
В С++ 11 мы в некоторых случаях ориентируемся на передачу объектов по значению, а в других - на const-reference. Однако это руководство зависит от реализации метода, а не только от его интерфейса и предполагаемого использования его клиентами.
Когда я пишу интерфейс, я не знаю, как он будет реализован. Есть ли хорошее правило для написания сигнатур методов? Например, в следующем фрагменте кода следует использовать Bar1
или Bar2
?
class IFoo
{
public:
virtual void Bar1(std::string s) = 0;
virtual void Bar2(const std::string& s) = 0;
};
Вы можете перестать читать здесь, если согласитесь, что правильная подпись зависит от реализации. Вот пример, который показывает, почему я так считаю.
В следующем примере мы должны передать строку по значению:
class Foo
{
std::string bar;
Foo(std::string byValue)
: bar(std::move(byValue))
{
}
};
Теперь мы можем эффективно создавать Foo во всех случаях:
Foo foo1("Hello world"); // create once, move once
Foo foo2(s); // the programmer wants to copy s. One copy and one move
Foo foo3(std::move(t)); // the programmer does not need t anymore. No copy at all
В других случаях мы предпочитаем передавать объекты по ссылке const. Например, в следующем случае мы никогда не хотим копировать/хранить аргумент, просто используйте его методы:
void DoStuff(const std::string& byRef)
{
std::cout << byRef.length() << std::endl;
}
Все возможные способы использования вышеуказанного метода уже настолько эффективны, насколько это возможно.
Обновление
Я считаю, что забыл показать проблемы с альтернативой const-reference. Если вышеуказанный класс Foo
был реализован следующим образом:
class Foo
{
std::string bar;
Foo(const std::string& byRef)
: bar(byRef)
{
}
};
Тогда мы получили бы следующие результаты:
Foo foo1("Hello world"); // Here we would have one more copy of the string. It is less efficient.
Foo foo2(s); // One copy, like before
Foo foo3(std::move(t)); // Irrelevant here.
Алекс.
Ответы
Ответ 1
Здесь нет "теории всего". У вас все в порядке, есть проблема.
Я помню, как он некоторое время назад вернулся.
Мои выводы начались здесь:
Приложение против разработки платформы/библиотеки
Если ваши клиенты - разработчики, эта работа намного сложнее. Это не только сложнее, но нет четких указаний. Великие дизайнеры каркасов получили свой престиж, потому что им случалось рисковать, что окупилось. В то же время в альтернативной вселенной их риски могли не окупиться. Это потому, что оценка структуры зависит от направления ее растущего использования и субъективных мнений, о которых гораздо труднее рассуждать, чем в области приложений.
Таким образом, в этом случае нет четкого ответа. К счастью, я думаю, что вы заинтересованы в основном в разработке приложений здесь. Так что давайте перейдем к этому.
Начальная точка: мы разрабатываем приложения
Это имеет огромное значение. Потому что мы должны иметь гораздо лучшее представление о том, куда идет система, и какой код может оказаться полезным. Мы не пророки, но в то же время это предположение позволяет нам больше ценить нашу интуицию, основанную на наших знаниях требований и потребностях наших клиентов (по крайней мере, насколько мы могли понять).
В этот момент мы все еще можем разделить это на 2 случая:
Абстракция к реализации
Есть случаи, когда полезно или даже необходимо определять абстракцию перед реализацией. В таких случаях необходимо осознать, что для определения абстракции требуется гораздо больше исследований по этой проблеме. Например, является ли домен синхронным или асинхронным? Последовательный или параллельный? Высокий или низкий уровень? И другие гораздо более конкретные вопросы.
Некоторые экстремальные агропилы заставят вас поверить, что вы можете просто написать код и исправить его позже. Однако эта претензия очень легко сфальсифицирована, как только реальность попадает. Если вы найдете в этом надежду, я рекомендую вам проверить его самостоятельно и сообщить, если вы сделали какое-либо значительное открытие. Мой личный опыт и мысль о том, что я попытался применить этот вопрос, предполагают, что в крупных проектах этот подход очень проблематичен.
Вывод в этом случае состоит в том, что, если вам действительно нужно определить абстракцию вперед, тогда у вас уже должно быть очень хорошее представление об осуществлении. Лучшая идея, которую вы имеете о ней, тем выше вероятность того, что она действительно станет правильной абстракцией.
Реализация в абстракции
Это мой выбор по умолчанию. Это было сказано многими способами. "Рамки должны быть извлечены", "Извлечь", пока вы не упадете ", и даже" Конвенция по конфигурации "имеет некоторое сходство в концепции.
В основном это означает, что вы при необходимости применяете необходимые компоненты, но следите за тем, что происходит. Трюк здесь заключается в том, чтобы искать возможности для абстрактного в том, что фактически приносит вам пользу в плане развития и обслуживания.
Это часто возникает как класс, который делает то, что вы хотите, но больше. В этом случае вы абстрагируете пересечение в более общий случай. Вы повторяете этот процесс по мере необходимости во время разработки.
Важно не попасться и все еще держать ноги на земле. Я видел, что многие попытки абстракции не совпадают с точкой, где нет никакого способа рассуждать о ее имени и выводить его намерение, кроме чтения тысяч строк кода, которые его используют. Например, в текущей базе кода, над которой я работаю, тип, который должен был называться Image
, называется BinaryData
. Во всем коде есть попытки рассматривать его как конкретное (изображение), и как абстрактное понятие в то же время.
Подводя итог
Как я всегда напоминаю себе, лучшая передовая практика, которую вы можете использовать, - это приручить известные лучшие практики, чтобы соответствовать вашей проблеме, а не наоборот. Если вы не можете этого сделать, ну, может быть, проблема достаточно интересная, чтобы требовать дополнительного внимания и немного оригинальной мысли.
Ответ 2
Вы также можете указать перегрузку для Bar2
, которая принимает ссылку rvalue:
class IFoo
{
public:
virtual void Bar2(const std::string& s) = 0;
virtual void Bar2(std::string&& s)
{
Bar2(s); // calls the const& overload because s is an lvalue
}
};
По умолчанию опорная перегрузка rvalue просто вызывает опорную ссылку const lvalue. Но если конкретный подкласс может использовать ссылки rvalue, избыточная эталонная перегрузка rvalue может быть переопределена.
Ответ 3
Я считаю, что он должен определенно зависеть от реализации. Как вытекает из вашего вопроса, запрещая полностью "всегда лучшую" подпись, единственная разумная вещь - выбрать подпись таким образом, чтобы оптимизировать текущую реализацию. Если вы напишете интерфейс перед кодом - возьмите обоснованное предположение и попытайтесь маневрировать себе таким образом, чтобы вы могли дождаться первой реализации, прежде чем совершать подпись.
Оперативные слова здесь являются "первыми" и "текущими". Что произойдет, если вы ошибаетесь? Что произойдет, если на каком-то более позднем этапе подпись не позволит вашему коду быть оптимальным? Вот что вы можете сделать:
Нет обязательств
Если это достаточно скоро - просто измените его. Из определения понятия "нет обязательств", правильно?
Выполнено для API
Для конкретного примера предположим, что вы выбрали неверное, и пошли с этим:
virtual void DoStuff(std::string s) = 0;
Однако, как оказалось, копирование не требуется (так же, как и исходная реализация DoStuff
). Вот что вы можете сделать:
// stuff.h
virtual void DoStuff_Optimized(const std::string & s);
virtual void DoStuff(std::string s);
// stuff.cc
virtual void DoStuff_Optimized(const std::string & s);
{
// Fast implementation of DoStuff, no copying necessary
std::cout << s.length() << std::endl;
}
virtual void DoStuff(std::string s)
{
DoStuff_Optimized(s);
}
Существующие клиенты получат более низкую производительность. Новые клиенты могут использовать версию Optimized
.
Выполнено для ABI
На данный момент, к сожалению, вы ничего не можете сделать. Однако, если вы осторожны, вы можете следить за действиями "Committed to API". (В частности, мой пример не сохранит совместимость с ABI).