Рефакторинг с С++ 11
Учитывая новый набор инструментов, предоставляемый С++, многие программисты, стремясь упростить код, выразительность, эффективность, проскальзывать через свой старый код и сделать трюки (некоторые бессмысленные, некоторые успешные) для достижения своих целей. Не пытаясь потерять слишком много времени на таких трудах и просто сделать неинтрузивные и самодостаточные изменения, каковы лучшие практики?
Позвольте мне вычеркнуть очевидное:
-
Используйте авто для запуска циклов на основе итератора:
for (std::vector<foo>::const_iterator it(lala.begin()), ite(lala.end()); it != ite;
++it);
// becomes
for (auto it(lala.cbegin()), ite(lala.cend()); it != ite; ++it);
-
Используйте связь для нескольких назначений, которые просто создают строки кода C (как назначить сразу несколько значений в структуру?)
a = 1;
b = 2;
c = 3;
d = 4;
e = 5;
// becomes
std::tie(a, b, c, d, e) = std::make_tuple(1, 2, 3, 4, 5);
-
Чтобы сделать класс не наследуемым, просто объявите его "окончательным" и удалите код, который достиг такого поведения http://www.parashift.com/c++-faq/final-classes.html
-
Используйте ключевое слово delete, чтобы явно скрыть конструкторы/деструкторы вместо того, чтобы объявлять их приватными (например, код для создания объектов с кучей, не копируемых объектов и т.д.)
-
Поверните тривиальные функторы, созданные только для упрощения выполнения одного алгоритма STL в функциях lambda (помимо уменьшения загромождения кода у вас будут гарантированные встроенные вызовы)
-
Упростите обтекание RAII объекта с помощью умного указателя
-
Избавьтесь от bind1st, bind2nd и просто используйте bind
-
Заменить написанный вручную код для типов типов (Is_ptr_but_dont_call_for_const_ptrs < > и таких:)) со стандартным кодом, предоставленным type_traits >
-
Остановить включение форвардных заголовков для функциональности, теперь вложенных в STL (BOOST_STATIC_ASSERT vs static_assert)
-
Предоставить семантику перемещения в классы (хотя это не будет считаться грязным/быстрым/легким изменением)
-
Используйте nullptr, если это возможно, вместо макроса NULL и избавитесь от кода, который заполняет контейнеры указателей с 0, отлитыми от типа объекта
std::vector<foo*> f(23);
for (std::size_t i(0); i < 23; ++i)
{ f[i] = static_cast<foo*>(0); }
// becomes
std::vector<foo*> f(23, nullptr);
-
Очистить синтаксис доступа к данным для данных
std::vector<int> vec;
&vec[0]; // access data as a C-style array
vec.data(); // new way of saying the above
-
Заменить throw() с помощью noexcept (кроме исключения устаревшего исключения, вы получаете некоторые преимущества по скорости http://channel9.msdn.com/Events/GoingNative/2013/An-Effective-Cpp11-14-Sampler @00.29.42)
void some_func() noexcept; // more optimization options
void some_func() throw(); // fewer optimization options
void some_func() ; // fewer optimization options
-
Замените код, в котором вы нажимали tempory в контейнере, и надеялись, что оптимизатор будет удален от копии, с функцией "emplace" , где это возможно, чтобы отлично переправить аргумент и построить непосредственно объект в контейнер без временных.
vecOfPoints.push_back(Point(x,y,z)); // so '03
vecOfPoints.emplace_back(x, y, z); // no copy or move operations performed
UPDATE
Ответ Шафика Яхмура был по праву награжден за щедрость за то, что он получил наибольшее признание со стороны аудитории.
Ответ R Sahu был моим принятым, потому что комбинация функций, которые он предлагает, захватывает дух рефакторинга: сделать код более понятным и чистым, более простым и элегантным.
Ответы
Ответ 1
Я бы добавил делегирование конструкторов и инициализаторов членов класса в список.
Упрощение с помощью делегирования конструкторов и инициализации внутри класса
С С++ 03:
class A
{
public:
// The default constructor as well as the copy constructor need to
// initialize some of the members almost the same and call init() to
// finish construction.
A(double data) : id_(0), name_(), data_(data) {init();}
A(A const& copy) : id_(0), name_(), data_(copy.data_) {init();}
void init()
{
id_ = getNextID();
name_ = getDefaultName();
}
int id_;
string name_;
double data_;
};
С С++ 11:
class A
{
public:
// With delegating constructor, the copy constructor can
// reuse this constructor and avoid repetitive code.
// In-line initialization takes care of initializing the members.
A(double data) : data_(data) {}
A(A const& copy) : A(copy.data_) {}
int id_ = getNextID();
string name_ = getDefaultName();
double data_;
};
Ответ 2
1. Замена rand
Один из больших выигрышей в С++ 11 должен заменить использование rand()
всеми параметрами, доступными в случайный заголовок. Замена rand()
во многих случаях должна быть прямой.
Стефан Т. Лававей, вероятно, сделал этот момент самым сильным с его презентацией rand() считается вредным. Примеры показывают равномерное целочисленное распределение из [0,10]
с помощью rand()
:
#include <cstdlib>
#include <iostream>
#include <ctime>
int main()
{
srand(time(0)) ;
for (int n = 0; n < 10; ++n)
{
std::cout << (rand() / (RAND_MAX / (10 + 1) + 1)) << ", " ;
}
std::cout << std::endl ;
}
и используя std:: uniform_int_distrubution:
#include <iostream>
#include <random>
int main()
{
std::random_device rd;
std::mt19937 e2(rd());
std::uniform_int_distribution<> dist(0, 10);
for (int n = 0; n < 10; ++n) {
std::cout << dist(e2) << ", " ;
}
std::cout << std::endl ;
}
Наряду с этим следует перейти от std:: random_shuffle к std:: shuffle, который исходит из усилий Отменить rand и друзей. Это недавно было рассмотрено в вопросе SO Почему методы std:: shuffle устарели в С++ 14?.
Обратите внимание, что дистрибутивы не гарантируются для разных платформ.
2. Использование std:: to_string вместо std:: ostringstream или sprintf
С++ 11 предоставляет std:: to_string, который можно использовать для преобразования чисел в std::string, это создаст контент как эквивалентный std:: sprintf. Скорее всего, это будет использоваться вместо std:: ostringstream или snprintf
. Это более удобно, вероятно, не так много разницы в производительности, и мы можем видеть из Быстрое целое число для преобразования строк в С++. вероятно, гораздо более быстрые альтернативы, если производительность является основной проблемой:
#include <iostream>
#include <sstream>
#include <string>
int main()
{
std::ostringstream mystream;
mystream << 100 ;
std::string s = mystream.str();
std::cout << s << std::endl ;
char buff[12] = {0};
sprintf(buff, "%d", 100);
std::string s2( buff ) ;
std::cout << s2 << std::endl ;
std::cout << std::to_string( 100 ) << std::endl ;
}
3. Использование constexpr вместо метапрограмм шаблона
Если вы имеете дело с литералами, могут быть случаи, когда использование функций constexpr над метапрограммой шаблона может привести к более четкому и, возможно, компилятору кода. Статья Хотите скорость? Использование метапрограммирования constexpr! дает пример определения простого числа с использованием метапрограмм шаблонов:
struct false_type
{
typedef false_type type;
enum { value = 0 };
};
struct true_type
{
typedef true_type type;
enum { value = 1 };
};
template<bool condition, class T, class U>
struct if_
{
typedef U type;
};
template <class T, class U>
struct if_<true, T, U>
{
typedef T type;
};
template<size_t N, size_t c>
struct is_prime_impl
{
typedef typename if_<(c*c > N),
true_type,
typename if_<(N % c == 0),
false_type,
is_prime_impl<N, c+1> >::type >::type type;
enum { value = type::value };
};
template<size_t N>
struct is_prime
{
enum { value = is_prime_impl<N, 2>::type::value };
};
template <>
struct is_prime<0>
{
enum { value = 0 };
};
template <>
struct is_prime<1>
{
enum { value = 0 };
};
и используя функции constexpr:
constexpr bool is_prime_recursive(size_t number, size_t c)
{
return (c*c > number) ? true :
(number % c == 0) ? false :
is_prime_recursive(number, c+1);
}
constexpr bool is_prime_func(size_t number)
{
return (number <= 1) ? false : is_prime_recursive(number, 2);
}
Версия constexpr намного короче, проще понять и, по-видимому, намного лучше, чем реализация метапрограмм шаблона.
4. Использование инициализации члена класса для предоставления значений по умолчанию
Как недавно было рассмотрено в Была ли введена новая функция инициализации члена С++ 11 в объявлении, сделанной инициализацией списков устаревших?, чтобы инициализировать член класса, чтобы предоставлять значения по умолчанию и может упростить случаи, когда класс имеет несколько конструкторов.
Bjarne Stroustrup является хорошим примером в FAQ на С++ 11, он говорит:
Это экономит немного ввода, но реальные преимущества приходят в классы с несколькими конструкторами. Часто для всех конструкторов используется обычный инициализатор для члена:
и предоставляет пример элементов, которые имеют общий инициализатор:
class A {
public:
A(): a(7), b(5), hash_algorithm("MD5"), s("Constructor run") {}
A(int a_val) : a(a_val), b(5), hash_algorithm("MD5"), s("Constructor run") {}
A(D d) : a(7), b(g(d)), hash_algorithm("MD5"), s("Constructor run") {}
int a, b;
private:
HashingFunction hash_algorithm; // Cryptographic hash to be applied to all A instances
std::string s; // String indicating state in object lifecycle
};
и говорит:
Тот факт, что hash_algorithm и s каждый имеет один умолчаний, теряется в беспорядке кода и может легко стать проблемой во время обслуживания. Вместо этого мы можем исключить инициализацию членов данных:
class A {
public:
A(): a(7), b(5) {}
A(int a_val) : a(a_val), b(5) {}
A(D d) : a(7), b(g(d)) {}
int a, b;
private:
HashingFunction hash_algorithm{"MD5"}; // Cryptographic hash to be applied to all A instances
std::string s{"Constructor run"}; // String indicating state in object lifecycle
};
Обратите внимание, что в С++ 11 класс, использующий в инициализаторах члена класса, больше не является агрегатом, хотя это ограничение удалено в С++ 14.
5. Используйте целые типы фиксированной ширины из cstdint вместо ручных typedefs
Так как стандарт С++ 11 использует C99 в качестве нормативной ссылки, мы получаем фиксированные ширины целочисленных типов. Например:
int8_t
int16_t
int32_t
int64_t
intptr_t
Хотя некоторые из них необязательны, для точных целых чисел ширины применяется следующее из раздела C99 7.18.1.1
:
Эти типы являются необязательными. Однако, если реализация обеспечивает целочисленные типы с шириной 8, 16, 32 или 64 бита, без битов заполнения и (для подписанных типов), которые имеют представление двойного дополнения, оно должно определить соответствующие имена typedef.
Ответ 3
Для каждого синтаксиса:
std::vector<int> container;
for (auto const & i : container)
std::cout << i << std::endl;
Ответ 4
Используйте равномерный синтаксис инициализации
для инициализация переменных
widget w(x); // old
widget w{x}; // new
чтобы избежать таких проблем, как С++ наиболее неприятный синтаксический анализ (остальные причины, по которым новый способ превосходит, объясняются в связанной статье Herb Sutter)
Ответ 5
- Изменение
std::map
на std::unordered_map
и std::set
на
std::unordered_set
, где когда-либо порядок элементов контейнера не имеет значения, значительно повышает производительность.
- Использование
std::map::at
вместо использования синтаксиса с квадратной скобкой, когда вы хотите избежать непроизвольных вставок.
- Используйте шаблоны псевдонимов, если хотите
typedef
шаблоны.
- Использование списков инициализации вместо циклов для инициализации контейнеров STL.
- Замените массивы с фиксированным размером C на std:: array.
Ответ 6
Функция: std:: move
"Указать четкую разницу между копированием и перемещением ресурсов"
std::string tmp("move");
std::vector<std::string> v;
v.push_back(std::move(tmp));
//At this point tmp still be the valid object but in unspecified state as
// its resources has been moved and now stored in vector container.
Ответ 7
Этот пост в блоге предлагает Rule of Zero, если все владельцы в классе следуют принципу RAII, позволяя избавиться от правила трех /Four/Five в С++ 11.
Однако Скотт Мейерс показывает здесь, что явно не пишет деструктор, конструкторы копирования/перемещения и назначения могут вызвать тонкие проблемы, если вы слегка измените ваш код (скажем, для отладки). Затем он рекомендует явно объявить default (функция С++ 11) следующие функции:
~MyClass() = default;
MyClass( const MyClass& ) = default;
MyClass( MyClass&& ) = default;
MyClass& operator=( const MyClass& ) = default;
MyClass& operator=( MyClass&& ) = default;
Ответ 8
Оптимизируйте простые математические функции с помощью constexpr, особенно если они вызваны внутри внутренних циклов. Это позволит компилятору рассчитать их при компиляции, экономя время.
Пример
constexpr int fibonacci(int i) {
return i==0 ? 0 : (i==1 ? 1 : fibonacci(i-1) + fibonacci(i-2));
}
Другим примером является использование std::enable_if
, чтобы ограничить допустимые типы параметров шаблона в определенной функции/классе шаблона. Это сделало бы ваш код более безопасным (если вы не используете SFINAE для ограничения возможных аргументов шаблона в вашем старом коде), когда вы подразумеваете некоторое свойство типа шаблонов, и это всего лишь одна дополнительная строка кода
пример:
template
<
typename T,
std::enable_if< std::is_abstract<T>::value == false, bool>::type = false // extra line
>
void f(T t)
{
// do something that depends on the fact that std::is_abstract<T>::value == false
}
Обновление 1: Если у вас есть небольшой массив, размер которого известен во время компиляции, и вы хотите избежать накладных расходов на распределение кучи в std::vector (что означает: вы хотите, чтобы массив в стеке), вы только выбор в С++ 03 состоял в использовании массивов c-style. Измените это на std::array
. Это простое изменение, которое предоставляет вам много функционально присутствующих в распределении стека std::vector + (намного быстрее, чем распределение кучи, как я сказал ранее).
Ответ 9
Используйте интеллектуальные указатели. Обратите внимание, что в некоторых случаях по-прежнему есть веские причины иметь голой указатель, лучший способ проверить, должен ли указатель быть умным, - искать в нем delete
.
Не должно быть причин использовать new
. Замените каждый new
на make_shared
или make_unique
.
К сожалению, make_unique
не сделал это в стандарте С++ 11, лучшим решением IMO является его реализация (см. предыдущую ссылку) и поместите некоторые макросы для проверки версии __cplusplus
(make_unique
доступен на С++ 14).
Использование make_unique
и make_shared
действительно важно для обеспечения безопасности кода.
Ответ 10
-
Предпочитают перечисления с привязкой к незанятым перечислениям
-
В С++ 98 перечислениях нет перечислений для перечислений, таких как следующий фрагмент кода. Имена таких счетчиков относятся к области, содержащей перечисление, а именно ничто в этой области не может иметь одно и то же имя.
enum Color{ blue, green, yellow };
bool blue = false; // error: 'blue' redefinition
Однако, в С++ 11, scoped enums
может исправить эту проблему. scoped enum
объявляются var enum class
.
enum class Color{ blue, green, yellow };
bool blue = false; // fine, no other `blue` in scope
Color cc = blue; // error! no enumerator `blue` in this scope
Color cc = Color::blue; // fine
auto c = Color::blue; // fine
-
Перечислители scope enums
более строго типизированы. Но перечисления unscoped enums
неявно преобразуются в другие типы
enum Color{ blue, green, yellow };
std::vector<std::size_t> getVector(std::size_t x);
Color c = blue;
if (c < 10.1) { // compare Color with double !!
auto vec = getVector(c); // could be fine !!
}
Однако в этом случае scoped enums
будет недействительным.
enum class Color{ blue, green, yellow };
std::vector<std::size_t> getVector(std::size_t x);
Color c = Color::blue;
if (c < 10.1) { // error !
auto vec = getVector(c); // error !!
}
Закрепите его через static_cast
if (static_cast<double>(c) < 10.1) {
auto vec = getVector(static_cast<std::size_t>(c));
}
-
unscoped enums
может быть объявлен вперед.
enum Color; // error!!
enum class Color; // fine
-
Оба scoped
и unscoped
перечисления поддерживают спецификацию базового типа. По умолчанию для scoped enums
используется тип int
. unscoped enums
не имеют базового типа по умолчанию.
-
Использование Concurrency API
-
Предпочтение основано на задачах на основе потоков
Если вы хотите запустить функцию doAsyncWork
асинхронно, у вас есть два основных варианта. Один из них основан на потоках
int doAsyncWork();
std::thread t(doAsyncWork);
Другой объект основан на задачах.
auto fut = std::async(doAsyncWork);
Очевидно, мы можем получить возвращаемое значение doAsyncWork
с помощью задачи легче, чем на основе потоков. С подходом task-based
его легко, потому что будущее, возвращаемое из std::async
, предлагает функцию get. Функция get
еще важнее, если doAsyncWork
испускает исключение, потому что get
также предоставляет доступ к этому.
-
Thread-based
вызывает ручное управление исчерпанием потока, переподпиской, балансировкой нагрузки и адаптацией к новым платформам.
Но task-based
через std::async
со стандартной политикой запуска не страдает ни одним из этих недостатков.
Вот несколько ссылок:
Concurrency В С++
C/С++ Программирование Абстракции для Parallelism и Concurrency
Ответ 11
Использование ключевого слова переопределить
Отметьте виртуальные функции в производных классах как переопределенные (если они действительно переопределяют, конечно). Это может защитить от введения ошибок в будущем, например. путем изменения сигнатуры виртуальной функции в базовом классе и забывания изменить подпись во всех производных классах соответственно.