Методы расширения в С++
Я искал реализацию методов расширения в С++ и пришел к этому обсуждению comp.std.С++, в котором упоминается, что polymorphic_map
может использоваться для связанных методов с классом, но предоставленная ссылка кажется мертвой. Кто-нибудь знает, к чему относится этот ответ, или если есть другой способ расширить классы аналогично методам расширения (возможно, через некоторое использование mixins?).
Я знаю, что каноническое решение на C++ должно использовать свободные функции; это больше из любопытства, чем что-либо еще.
Ответы
Ответ 1
Различные языки подходят для развития по-разному. В частности, С# и Java имеют сильную точку зрения относительно OO, что приводит к тому, что все является объектом мышления (С# здесь немного слабее). В этом подходе методы расширения предоставляют простой способ расширения существующего объекта или интерфейса для добавления новых функций.
В С++ нет методов расширения, и они не нужны. При разработке С++ забывайте, что все это парадигма объекта, которая, кстати, ложна даже в Java/С# [*]. В С++ используется другое мышление, есть объекты, а объекты имеют операции, которые являются неотъемлемой частью объекта, но есть и другие операции, которые являются частью интерфейса и не должны быть частью класса. Обязательно прочитать Herb Sutter Что в классе?, где автор защищает (и я согласен), что вы можете легко расширить любой данный класс с помощью простых бесплатных функций.
В качестве конкретного простого примера стандартный шаблонный класс basic_ostream
имеет несколько методов-членов, чтобы сбрасывать содержимое некоторых примитивных типов, а затем он дополняется (также templated) свободными функциями, которые расширяют эту функциональность до других типов посредством используя существующий публичный интерфейс. Например, std::cout << 1;
реализуется как функция-член, а std::cout << "Hi";
- свободная функция, реализованная в терминах других более основных элементов.
Расширяемость в С++ достигается с помощью бесплатных функций, а не путем добавления новых методов к существующим объектам.
[*] Все не объект.
В данном домене будет содержаться набор реальных объектов, которые могут быть смоделированы, и операции, которые могут быть применены к ним, в некоторых случаях эти операции будут частью объекта, но в некоторых других случаях они не будут. В частности, вы найдете классы утилиты на языках, которые утверждают, что все является объектом, а эти классы полезности - не что иное, как слой, который пытается скрыть тот факт, что эти методы не принадлежат к какому-либо конкретному объекту.
Даже некоторые операции, которые реализуются как функции-члены, на самом деле не являются объектами объекта. Рассмотрим дополнение для класса чисел Complex
, как sum
(или +
) больше операции над первым аргументом, чем вторая? Почему a.sum(b);
или b.sum(a)
, не должно ли оно быть sum( a, b )
?
Принуждение операций к элементам-членам фактически создает странные эффекты - но мы просто используем их: a.equals(b);
и b.equals(a);
могут иметь совершенно разные результаты, даже если реализация equals
полностью симметрична. (Рассмотрим, что происходит, когда либо a
, либо b
является нулевым указателем)
Ответ 2
Подход к библиотеке расширенного диапазона использует оператор |().
r | filtered(p);
Я также могу написать обрезку для строки следующим образом.
#include <string>
namespace string_extension {
struct trim_t {
std::string operator()(const std::string& s) const
{
...
return s;
}
};
const trim_t trim = {};
std::string operator|(const std::string& s, trim_t f)
{
return f(s);
}
} // namespace string_extension
int main()
{
const std::string s = " abc ";
const std::string result = s | string_extension::trim;
}
Ответ 3
Короткий ответ заключается в том, что вы не можете этого сделать. Долгий ответ заключается в том, что вы можете имитировать его, но помните, что вам придется создавать много кода в качестве обходного пути (на самом деле, я не думаю, что есть элегантное решение).
В обсуждении очень сложное обходное решение предоставляется с использованием оператора (что, на мой взгляд, является плохим идеей). Я предполагаю, что решение, предоставленное в мертвой ссылке, было более менее похожим (поскольку оно было основано на операторе |).
Это основано на способности иметь более или менее то же самое, что и метод расширения с операторами. Например, если вы хотите перегрузить оператор ostream < < для вашего нового класса Foo вы можете сделать:
class Foo {
friend ostream &operator<<(ostream &o, const Foo &foo);
// more things...
};
ostream &operator<<(ostream &o, const Foo &foo)
{
// write foo info to o
}
Как я уже сказал, это единственный подобный механизм, доступный в С++ для методов расширения. Если вы можете, естественно, перевести свою функцию на перегруженный оператор, тогда это нормально. Единственная другая возможность - искусственно перегрузить оператора, который не имеет ничего общего с вашей целью, но это заставит вас писать очень запутывающий код.
Самый похожий подход, о котором я могу думать, означал бы создать класс расширения и создать там свои новые методы. К сожалению, это означает, что вам нужно "адаптировать" свои объекты:
class stringext {
public:
stringext(std::string &s) : str( &s )
{}
string trim()
{ ...; return *str; }
private:
string * str;
};
И затем, когда вы захотите сделать это:
void fie(string &str)
{
// ...
cout << stringext( str ).trim() << endl;
}
Как сказано, это не идеально, и я не думаю, что такое идеальное решение существует.
К сожалению.
Ответ 4
Это самое близкое, что я когда-либо видел в методах расширения на С++. Лично мне нравится, как это можно использовать, и, возможно, это самое близкое к тому, что мы можем получить в методах расширения на этом языке. Но есть некоторые недостатки:
- Может быть сложно реализовать
- Приоритет оператора может быть не очень приятным несколько раз, это может вызвать неожиданности
Решение:
#include <iostream>
using namespace std;
class regular_class {
public:
void simple_method(void) const {
cout << "simple_method called." << endl;
}
};
class ext_method {
private:
// arguments of the extension method
int x_;
public:
// arguments get initialized here
ext_method(int x) : x_(x) {
}
// just a dummy overload to return a reference to itself
ext_method& operator-(void) {
return *this;
}
// extension method body is implemented here. The return type of this op. overload
// should be the return type of the extension method
friend const regular_class& operator<(const regular_class& obj, const ext_method& mthd) {
cout << "Extension method called with: " << mthd.x_ << " on " << &obj << endl;
return obj;
}
};
int main()
{
regular_class obj;
cout << "regular_class object at: " << &obj << endl;
obj.simple_method();
obj<-ext_method(3)<-ext_method(8);
return 0;
}
Это не мое личное изобретение, недавно мой друг отправил мне письмо, он сказал, что получил его из списка рассылки в университете.
Ответ 5
Вы можете включить своего рода методы расширения для своего собственного класса/структуры или для определенного типа в некоторой области. Смотрите примерное решение ниже.
class Extensible
{
public:
template<class TRes, class T, class... Args>
std::function<TRes(Args...)> operator|
(std::function<TRes(T&, Args...)>& extension)
{
return [this, &extension](Args... args) -> TRes
{
return extension(*static_cast<T*>(this), std::forward<Args>(args)...);
};
}
};
Затем унаследуйте свой класс от этого и используйте как
class SomeExtensible : public Extensible { /*...*/ };
std::function<int(SomeExtensible&, int)> fn;
SomeExtensible se;
int i = (se | fn)(4);
Или вы можете объявить этот оператор в файле cpp или пространстве имен.
//for std::string, for example
template<class TRes, class... Args>
std::function<TRes(Args...)> operator|
(std::string& s, std::function<TRes(std::string&, Args...)>& extension)
{
return [&s, &extension](Args... args) -> TRes
{
return extension(s, std::forward<Args>(args)...);
};
}
std::string s = "newStr";
std::function<std::string(std::string&)> init = [](std::string& s) {
return s = "initialized";
};
(s | init)();
Или даже обернуть это в макрос (я знаю, это вообще плохая идея, тем не менее, вы можете):
#define ENABLE_EXTENSIONS_FOR(x) \
template<class TRes, class... Args> \
std::function<TRes(Args...)> operator| (x s, std::function<TRes(x, Args...)>& extension) \
{ \
return [&s, &extension](Args... args) -> TRes \
{ \
return extension(s, std::forward<Args>(args)...); \
}; \
}
ENABLE_EXTENSIONS_FOR(std::vector<int>&);
Ответ 6
Чтобы подробнее узнать о @Akira answer, operator|
может использоваться для расширения существующих классов функциями, которые также принимают параметры. Вот пример, который я использую для расширения библиотеки Xerces XML функциями поиска, которые можно легко объединить:
#pragma once
#include <string>
#include <stdexcept>
#include <xercesc/dom/DOMElement.hpp>
#define _U16C // macro that converts string to char16_t array
XERCES_CPP_NAMESPACE_BEGIN
struct FindFirst
{
FindFirst(const std::string& name);
DOMElement * operator()(const DOMElement &el) const;
DOMElement * operator()(const DOMElement *el) const;
private:
std::string m_name;
};
struct FindFirstExisting
{
FindFirstExisting(const std::string& name);
DOMElement & operator()(const DOMElement &el) const;
private:
std::string m_name;
};
inline DOMElement & operator|(const DOMElement &el, const FindFirstExisting &f)
{
return f(el);
}
inline DOMElement * operator|(const DOMElement &el, const FindFirst &f)
{
return f(el);
}
inline DOMElement * operator|(const DOMElement *el, const FindFirst &f)
{
return f(el);
}
inline FindFirst::FindFirst(const std::string & name)
: m_name(name)
{
}
inline DOMElement * FindFirst::operator()(const DOMElement &el) const
{
auto list = el.getElementsByTagName(_U16C(m_name));
if (list->getLength() == 0)
return nullptr;
return static_cast<DOMElement *>(list->item(0));
}
inline DOMElement * FindFirst::operator()(const DOMElement *el) const
{
if (el == nullptr)
return nullptr;
auto list = el->getElementsByTagName(_U16C(m_name));
if (list->getLength() == 0)
return nullptr;
return static_cast<DOMElement *>(list->item(0));
}
inline FindFirstExisting::FindFirstExisting(const std::string & name)
: m_name(name)
{
}
inline DOMElement & FindFirstExisting::operator()(const DOMElement & el) const
{
auto list = el.getElementsByTagName(_U16C(m_name));
if (list->getLength() == 0)
throw runtime_error(string("Missing element with name ") + m_name);
return static_cast<DOMElement &>(*list->item(0));
}
XERCES_CPP_NAMESPACE_END
Это можно использовать так:
auto packetRate = *elementRoot | FindFirst("Header") | FindFirst("PacketRate");
auto &decrypted = *elementRoot | FindFirstExisting("Header") | FindFirstExisting("Decrypted");