Скрытые возможности C++?
Не любите С ++, когда речь идет о "скрытых особенностях" вопросов? Подумал, я бы выбросил это там. Каковы некоторые из скрытых возможностей C++?
64 ответа
Большинство программистов на C++ знакомы с троичным оператором:
x = (y < 0) ? 10 : 20;
Тем не менее, они не понимают, что это может быть использовано в качестве lvalue:
(a == 0 ? a : b) = 1;
что является сокращением для
if (a == 0)
a = 1;
else
b = 1;
Используйте с осторожностью:-)
Вы можете поместить URI в источник C++ без ошибок. Например:
void foo() {
http://stackru.com/
int bar = 4;
...
}
Указатель арифметики.
Программисты C++ предпочитают избегать указателей из-за ошибок, которые могут быть внесены.
Самый крутой C++, который я когда-либо видел? Аналоговые литералы.
Я согласен с большинством сообщений: C++ - это мультипарадигмальный язык, поэтому "скрытые" функции, которые вы найдете (кроме "неопределенного поведения", которого следует избегать любой ценой), представляют собой разумное использование средств.
Большинство из этих возможностей - не встроенные функции языка, а основанные на библиотеках.
Наиболее важным является RAII, который многие годы игнорировался разработчиками C++ из мира Си. Перегрузка операторов часто является неправильно понятой функцией, которая включает как поведение, подобное массиву (оператор индекса), операции, подобные указателю (интеллектуальные указатели), так и встроенные операции (умножающие матрицы).
Использование исключений часто затруднительно, но при некоторой работе может создать действительно надежный код с помощью спецификаций безопасности исключений (включая код, который не будет давать сбой или который будет иметь функции, подобные коммиту, которые будут успешными или вернутся к его первоначальное состояние).
Самая известная из "скрытых" функций C++ - это метапрограммирование шаблонов, поскольку оно позволяет вам частично или полностью выполнять вашу программу во время компиляции, а не во время выполнения. Это сложно, хотя, и вы должны иметь четкое представление о шаблонах, прежде чем пытаться это.
Другие используют множественную парадигму для создания "способов программирования" вне предка C++, то есть C.
Используя функторы, вы можете моделировать функции с дополнительной безопасностью типов и сохранением состояния. Используя шаблон команды, вы можете отложить выполнение кода. Большинство других шаблонов проектирования могут быть легко и эффективно реализованы в C++ для создания альтернативных стилей кодирования, которые не должны входить в список "официальных парадигм C++".
Используя шаблоны, вы можете создавать код, который будет работать с большинством типов, включая тот, который вы не думали вначале. Вы также можете повысить безопасность типов (например, автоматизированная безопасность типов malloc / realloc / free). Объектные возможности C++ действительно мощные (и, следовательно, опасные, если используются небрежно), но даже динамический полиморфизм имеет свою статическую версию в C++: CRTP.
Я обнаружил, что большинство книг типа "Эффективный C++" от Скотта Мейерса или книги типа "Exceptional C++" от Херба Саттера, как легко читаемые, так и довольно ценные сведения об известных и менее известных функциях C++.
Среди моих предпочтений один, который должен заставить волосы любого Java-программиста подняться от ужаса: в C++ наиболее объектно-ориентированный способ добавить функцию к объекту - использовать функцию, не являющуюся членом, а не членом. функция (т.е. метод класса), потому что:
В C++ интерфейс класса - это и функции-члены, и функции, не являющиеся членами, в одном и том же пространстве имен
не являющиеся друзьями функции, не являющиеся членами, не имеют привилегированного доступа к внутреннему классу. Таким образом, использование функции-члена над не-членом, не являющимся другом, ослабит инкапсуляцию класса.
Это не перестает удивлять даже опытных разработчиков.
(Источник: среди прочих, онлайн-гуру недели Херба Саттера № 84: http://www.gotw.ca/gotw/084.htm)
Одной из особенностей языка, которую я считаю несколько скрытой, потому что я никогда не слышал об этом за все время, пока я учился в школе, является псевдоним пространства имен. Это не было доведено до моего сведения, пока я не натолкнулся на примеры в документации по бусту. Конечно, теперь, когда я знаю об этом, вы можете найти его в любой стандартной справке C++.
namespace fs = boost::filesystem;
fs::path myPath( strPath, fs::native );
Не только переменные могут быть объявлены в части инициализации for
цикл, но также классы и функции.
for(struct { int a; float b; } loop = { 1, 2 }; ...; ...) {
...
}
Это позволяет использовать несколько переменных разных типов.
Оператор массива является ассоциативным.
[8] является синонимом *(A + 8). Поскольку сложение ассоциативно, его можно переписать как *(8 + A), что является синонимом..... 8[A]
Вы не сказали, полезно...:-)
Мало что известно, что союзы тоже могут быть шаблонами:
template<typename From, typename To>
union union_cast {
From from;
To to;
union_cast(From from)
:from(from) { }
To getTo() const { return to; }
};
И они могут иметь конструкторы и функции-члены тоже. Просто ничего, что связано с наследованием (включая виртуальные функции).
C++ - это стандарт, не должно быть никаких скрытых возможностей...
C++ - это мультипарадигмальный язык, вы можете поставить свои последние деньги на скрытые возможности. Один пример из многих: шаблонное метапрограммирование. Никто в комитете по стандартизации не предполагал, что существует подъязык на языке Тьюринга, который выполняется во время компиляции.
Еще одна скрытая возможность, которая не работает в C, - это функциональность +
оператор. Вы можете использовать его, чтобы продвигать и разлагать все виды вещей
Преобразование перечисления в целое число
+AnEnumeratorValue
И ваше значение перечислителя, которое ранее имело свой тип перечисления, теперь имеет идеальный целочисленный тип, который может соответствовать его значению. Вручную вы вряд ли узнаете этот тип! Это необходимо, например, когда вы хотите реализовать перегруженный оператор для вашего перечисления.
Получить значение из переменной
Вы должны использовать класс, который использует статический инициализатор в классе без определения вне класса, но иногда он не может связать? Оператор может помочь создать временный объект, не делая предположений или зависимостей от его типа.
struct Foo {
static int const value = 42;
};
// This does something interesting...
template<typename T>
void f(T const&);
int main() {
// fails to link - tries to get the address of "Foo::value"!
f(Foo::value);
// works - pass a temporary value
f(+Foo::value);
}
Распад массива на указатель
Вы хотите передать два указателя на функцию, но она просто не будет работать? Оператор может помочь
// This does something interesting...
template<typename T>
void f(T const& a, T const& b);
int main() {
int a[2];
int b[3];
f(a, b); // won't work! different values for "T"!
f(+a, +b); // works! T is "int*" both time
}
Время жизни временных, связанных с постоянными ссылками - это то, о чем мало кто знает. Или, по крайней мере, это мое любимое знание C++, о котором большинство людей не знают.
const MyClass& x = MyClass(); // temporary exists as long as x is in scope
Хорошей функцией, которая не часто используется, является функциональный блок try-catch:
int Function()
try
{
// do something here
return 42;
}
catch(...)
{
return -1;
}
Основное использование будет переводить исключение в другой класс исключений и перебрасывать, или переводить между исключениями и обработкой кода ошибки на основе возврата.
Многие знают о identity
/ id
метафункция, но есть хороший вариант использования для случаев, не связанных с шаблоном: легкость написания объявлений:
// void (*f)(); // same
id<void()>::type *f;
// void (*f(void(*p)()))(int); // same
id<void(int)>::type *f(id<void()>::type *p);
// int (*p)[2] = new int[10][2]; // same
id<int[2]>::type *p = new int[10][2];
// void (C::*p)(int) = 0; // same
id<void(int)>::type C::*p = 0;
Это помогает расшифровать объявления C++ очень сильно!
// boost::identity is pretty much the same
template<typename T>
struct id { typedef T type; };
Довольно скрытая особенность заключается в том, что вы можете определять переменные в условии if, и его область действия будет охватывать только блоки if и else:
if(int * p = getPointer()) {
// do something
}
Некоторые макросы используют это, например, чтобы обеспечить некоторую "заблокированную" область видимости, например:
struct MutexLocker {
MutexLocker(Mutex&);
~MutexLocker();
operator bool() const { return false; }
private:
Mutex &m;
};
#define locked(mutex) if(MutexLocker const& lock = MutexLocker(mutex)) {} else
void someCriticalPath() {
locked(myLocker) { /* ... */ }
}
Также BOOST_FOREACH использует его под капотом. Чтобы завершить это, это возможно не только в if, но и в переключателе:
switch(int value = getIt()) {
// ...
}
и в цикле while:
while(SomeThing t = getSomeThing()) {
// ...
}
(а также в состоянии). Но я не слишком уверен, что все это полезно:)
Предотвращение оператора запятой от вызова перегрузки оператора
Иногда вы правильно используете оператор запятой, но вы хотите, чтобы ни один пользовательский оператор запятой не мешал, потому что, например, вы полагаетесь на точки последовательности между левой и правой сторонами или хотите убедиться, что ничто не мешает желаемому действие. Это где void()
вступает в игру:
for(T i, j; can_continue(i, j); ++i, void(), ++j)
do_code(i, j);
Проигнорируйте заполнители, которые я поместил для условия и кода. Что важно, так это void()
, который заставляет компилятор использовать встроенный оператор запятой. Это может быть полезно при реализации классов признаков, иногда тоже.
Инициализация массива в конструкторе. Например, в классе, если у нас есть массив int
как:
class clName
{
clName();
int a[10];
};
Мы можем инициализировать все элементы в массиве по умолчанию (здесь все элементы массива равны нулю) в конструкторе как:
clName::clName() : a()
{
}
Вы можете получить доступ к защищенным данным и членам-функциям любого класса, без неопределенного поведения и с ожидаемой семантикой. Читайте дальше, чтобы увидеть, как. Читайте также отчет о дефекте по этому поводу.
Обычно C++ запрещает вам доступ к нестатическим защищенным членам объекта класса, даже если этот класс является вашим базовым классом.
struct A {
protected:
int a;
};
struct B : A {
// error: can't access protected member
static int get(A &x) { return x.a; }
};
struct C : A { };
Это запрещено: вы и компилятор не знаете, на что действительно указывает ссылка. Это может быть C
объект, в этом случае класс B
не имеет никакого дела и понятия о своих данных. Такой доступ предоставляется только в том случае, если x
является ссылкой на производный класс или производный от него. И это может позволить произвольному куску кода прочитать любой защищенный элемент, просто создав "выбрасывающий" класс, который считывает элементы, например, std::stack
:
void f(std::stack<int> &s) {
// now, let's decide to mess with that stack!
struct pillager : std::stack<int> {
static std::deque<int> &get(std::stack<int> &s) {
// error: stack<int>::c is protected
return s.c;
}
};
// haha, now let's inspect the stack's middle elements!
std::deque<int> &d = pillager::get(s);
}
Конечно, как вы видите, это нанесет слишком большой ущерб. Но теперь указатели членов позволяют обойти эту защиту! Ключевым моментом является то, что тип указателя на член привязан к классу, который фактически содержит указанный член, а не к классу, который вы указали при получении адреса. Это позволяет нам обойти проверку
struct A {
protected:
int a;
};
struct B : A {
// valid: *can* access protected member
static int get(A &x) { return x.*(&B::a); }
};
struct C : A { };
И, конечно же, это также работает с std::stack
пример.
void f(std::stack<int> &s) {
// now, let's decide to mess with that stack!
struct pillager : std::stack<int> {
static std::deque<int> &get(std::stack<int> &s) {
return s.*(pillager::c);
}
};
// haha, now let's inspect the stack's middle elements!
std::deque<int> &d = pillager::get(s);
}
Это будет еще проще с использованием объявления в производном классе, которое делает имя члена общедоступным и ссылается на члена базового класса.
void f(std::stack<int> &s) {
// now, let's decide to mess with that stack!
struct pillager : std::stack<int> {
using std::stack<int>::c;
};
// haha, now let's inspect the stack's middle elements!
std::deque<int> &d = s.*(&pillager::c);
}
Оооо, вместо этого я могу составить список ненависти к домашним животным:
- Деструкторы должны быть виртуальными, если вы собираетесь использовать полиморфно
- Иногда члены инициализируются по умолчанию, иногда нет
- Локальные предложения не могут использоваться в качестве параметров шаблона (делает их менее полезными)
- Спецификаторы исключений: выглядят полезными, но не являются
- Перегрузки функций скрывают функции базового класса с разными сигнатурами.
- нет никакой полезной стандартизации для интернационализации (переносимая стандартная широкая кодировка, кто-нибудь? Придется подождать до C++0x)
На положительной стороне
- скрытая особенность: функция try блоков. К сожалению, я не нашел применения для этого. Да, я знаю, почему они добавили это, но вы должны перебросить конструктор, который делает это бессмысленным.
- Стоит внимательно посмотреть на гарантии STL о валидности итераторов после модификации контейнера, что может позволить вам сделать несколько более приятных циклов.
- Повышение - это не секрет, но его стоит использовать.
- Оптимизация возвращаемого значения (не очевидно, но это определенно разрешено стандартом)
- Функторы или объекты-функции, или оператор (). Это широко используется STL. на самом деле не секрет, но это изящный побочный эффект перегрузки операторов и шаблонов.
Скрытые возможности:
- Чистые виртуальные функции могут иметь реализацию. Типичный пример, чистый виртуальный деструктор.
Если функция выдает исключение, не указанное в спецификациях исключений, но функция имеет
std::bad_exception
в спецификации исключений исключение преобразуется вstd::bad_exception
и выбрасывается автоматически. Таким образом, вы, по крайней мере, будете знать, чтоbad_exception
был брошен. Узнайте больше здесь.блоки функций
Ключевое слово template в устранении неоднозначности typedefs в шаблоне класса. Если имя специалиста шаблона члена появляется после
.
,->
, или же::
оператор, и это имя имеет явно определенные параметры шаблона, префикс имени члена элемента с ключевым словом шаблона. Узнайте больше здесь.Значения параметров функции по умолчанию могут быть изменены во время выполнения. Узнайте больше здесь.
A[i]
работает так же хорошо, какi[A]
Временные экземпляры класса могут быть изменены! Неконстантная функция-член может быть вызвана для временного объекта. Например:
struct Bar { void modify() {} } int main (void) { Bar().modify(); /* non-const function invoked on a temporary. */ }
Узнайте больше здесь.
Если есть два разных типа до и после
:
в троице (?:
) оператор выражения, то результирующий тип выражения является наиболее общим из двух. Например:void foo (int) {} void foo (double) {} struct X { X (double d = 0.0) {} }; void foo (X) {} int main(void) { int i = 1; foo(i ? 0 : 0.0); // calls foo(double) X x; foo(i ? 0.0 : x); // calls foo(X) }
Другая скрытая особенность заключается в том, что вы можете вызывать объекты классов, которые можно преобразовать в указатели на функции или ссылки. Разрешение перегрузки выполняется по их результатам, и аргументы отлично передаются.
template<typename Func1, typename Func2>
class callable {
Func1 *m_f1;
Func2 *m_f2;
public:
callable(Func1 *f1, Func2 *f2):m_f1(f1), m_f2(f2) { }
operator Func1*() { return m_f1; }
operator Func2*() { return m_f2; }
};
void foo(int i) { std::cout << "foo: " << i << std::endl; }
void bar(long il) { std::cout << "bar: " << il << std::endl; }
int main() {
callable<void(int), void(long)> c(foo, bar);
c(42); // calls foo
c(42L); // calls bar
}
Они называются "суррогатными функциями вызова".
map::operator[]
создает запись, если ключ отсутствует, и возвращает ссылку на значение записи, созданное по умолчанию. Таким образом, вы можете написать:
map<int, string> m;
string& s = m[42]; // no need for map::find()
if (s.empty()) { // assuming we never store empty values in m
s.assign(...);
}
cout << s;
Я поражен тем, как много C++ программистов не знают этого.
Помещение функций или переменных в безымянное пространство имен исключает использование static
ограничить их областью файла.
Определение обычных функций друзей в шаблонах классов требует особого внимания:
template <typename T>
class Creator {
friend void appear() { // a new function ::appear(), but it doesn't
… // exist until Creator is instantiated
}
};
Creator<void> miracle; // ::appear() is created at this point
Creator<double> oops; // ERROR: ::appear() is created a second time!
В этом примере два разных экземпляра создают два идентичных определения - прямое нарушение ODR
Поэтому мы должны убедиться, что параметры шаблона шаблона класса присутствуют в типе любой функции-друга, определенной в этом шаблоне (если мы не хотим предотвратить более одного создания шаблона класса в определенном файле, но это довольно маловероятно). Давайте применим это к варианту нашего предыдущего примера:
template <typename T>
class Creator {
friend void feed(Creator<T>*){ // every T generates a different
… // function ::feed()
}
};
Creator<void> one; // generates ::feed(Creator<void>*)
Creator<double> two; // generates ::feed(Creator<double>*)
Отказ от ответственности: я вставил этот раздел из шаблонов C++: Полное руководство / Раздел 8.4
Пустые функции могут возвращать пустые значения.
Малоизвестно, но следующий код подходит
void f() { }
void g() { return f(); }
А также следующий странно выглядящий
void f() { return (void)"i'm discarded"; }
Зная об этом, вы можете воспользоваться в некоторых областях. Один пример: void
функции не могут возвращать значение, но вы также не можете просто ничего не возвращать, потому что они могут быть созданы с помощью non-void. Вместо сохранения значения в локальной переменной, что приведет к ошибке для void
, просто верните значение напрямую
template<typename T>
struct sample {
// assume f<T> may return void
T dosomething() { return f<T>(); }
// better than T t = f<T>(); /* ... */ return t; !
};
Считать файл в вектор строк:
vector<string> V;
copy(istream_iterator<string>(cin), istream_iterator<string>(),
back_inserter(V));
Вы можете шаблон битовых полей.
template <size_t X, size_t Y>
struct bitfield
{
char left : X;
char right : Y;
};
Мне еще предстоит придумать какую-либо цель для этого, но это точно, черт возьми, удивило меня.
Одна из самых интересных грамматик любых языков программирования.
Три из этих вещей принадлежат друг другу, а две - это нечто совершенно другое...
SomeType t = u;
SomeType t(u);
SomeType t();
SomeType t;
SomeType t(SomeType(u));
Все кроме третьего и пятого определяют SomeType
объект в стеке и инициализировать его (с u
в первых двух случаях и конструктор по умолчанию в четвертом. Третий - объявление функции, которая не принимает параметров и возвращает SomeType
, Пятый аналогично объявляет функцию, которая принимает один параметр по значению типа SomeType
названный u
,
Избавление от предварительных деклараций:
struct global
{
void main()
{
a = 1;
b();
}
int a;
void b(){}
}
singleton;
Написание switch-операторов с операторами::
string result =
a==0 ? "zero" :
a==1 ? "one" :
a==2 ? "two" :
0;
Делать все в одной строке:
void a();
int b();
float c = (a(),b(),1.0f);
Обнуление структур без memset:
FStruct s = {0};
Нормализация / обтекание значений угла и времени:
int angle = (short)((+180+30)*65536/360) * 360/65536; //==-150
Назначение ссылок:
struct ref
{
int& r;
ref(int& r):r(r){}
};
int b;
ref a(b);
int c;
*(int**)&a = &c;
Тернарный условный оператор ?:
требует, чтобы его второй и третий операнды имели "приятные" типы (говоря неформально). Но это требование имеет одно исключение (каламбур): второй или третий операнд может быть выражением броска (которое имеет тип void
), независимо от типа другого операнда.
Другими словами, можно написать следующие точно допустимые выражения C++, используя ?:
оператор
i = a > b ? a : throw something();
Кстати, тот факт, что бросить выражение на самом деле является выражением (типа void
), а не утверждение - еще одна малоизвестная особенность языка C++. Это означает, среди прочего, что следующий код является абсолютно допустимым
void foo()
{
return throw something();
}
хотя нет особого смысла делать это таким образом (возможно, в некотором общем шаблонном коде это может пригодиться).
Правило доминирования полезно, но мало известно. Это говорит о том, что даже если в неуникальном пути через решетку базового класса поиск имени частично скрытого члена уникален, если член принадлежит виртуальному базовому классу:
struct A { void f() { } };
struct B : virtual A { void f() { cout << "B!"; } };
struct C : virtual A { };
// name-lookup sees B::f and A::f, but B::f dominates over A::f !
struct D : B, C { void g() { f(); } };
Я использовал это для реализации поддержки выравнивания, которая автоматически определяет самое строгое выравнивание с помощью правила доминирования.
Это относится не только к виртуальным функциям, но и к именам typedef, статическим / не виртуальным членам и всем остальным. Я видел, как это использовалось для реализации перезаписываемых черт в метапрограммах.