Есть ли способ использовать специализацию шаблона для отделения new от new[]?

У меня есть класс автоматического указателя, и в конструкторе я передаю указатель. Я хочу иметь возможность отделять new от new[] в конструкторе, чтобы я мог правильно вызывать delete или delete[] в деструкторе. Можно ли это сделать с помощью специализации шаблона? Я не хочу передавать логическое значение в конструкторе.

    template <typename T>
    class MyAutoPtr
    {
    public:
      MyAutoPtr(T* aPtr);
    };

// in use:
MyAutoPtr<int> ptr(new int);
MyAutoPtr<int> ptr2(new int[10]);

person Marlon    schedule 07.04.2010    source источник
comment
В любом случае, хороший вопрос: это действительно заставляет вас желать правильной обработки array внутри самого языка :(   -  person Matthieu M.    schedule 07.04.2010
comment
Вы не должны использовать логический параметр для различения типов. Поскольку решение принимается во время компиляции, вы должны выразить его в типе, то есть вы создаете один класс интеллектуальных указателей для обычных указателей и один для массивов.   -  person Björn Pollex    schedule 07.04.2010


Ответы (7)


std::unique_ptr в C++0x будет иметь специализацию для динамических массивов, как показано ниже. Однако создание соответствующего экземпляра будет задачей пользователя. На уровне языка невозможно отличить один указатель от другого.

template <class T>
class pointer
{
    T* p;
public:
    pointer(T* ptr = 0): p(ptr) {}
    ~pointer() { delete p; }
    //... rest of pointer interface
};

template <class T>
class pointer<T[]>
{
    T* p;
public:
    pointer(T* ptr = 0): p(ptr) {}
    ~pointer() { delete [] p; }
    //... rest of pointer and array interface
};

int main()
{
    pointer<int> single(new int);
    pointer<int[]> array(new int[10]);
}

Кроме того, может быть не очень хорошо загружать один класс такими разными задачами. Например, у boost есть shared_ptr и shared_array.

person visitor    schedule 07.04.2010
comment
На самом деле я не понимаю, почему у Boost есть оба, поскольку shared_ptr использует параметр Deleter при построении (по умолчанию, если его нет), чтобы вы могли иметь его для случая с массивом. - person Matthieu M.; 07.04.2010
comment
Одна вещь, которую вы хотели бы получить от указателя на массив, это operator[]. И тогда shared_array<int> p(new int[10]) может быть удобным сокращением для shared_ptr<int> p(new int[10], array_deleter<int>());, даже если он вполне может использовать shared_ptr с удалятелем под капотом. Вообще я думаю, что есть большая разница между наличием одного объекта и массива: если функция принимает shared_ptr<T>, будет ли она также обрабатывать динамические массивы? - person visitor; 07.04.2010
comment
этот способ имеет для меня наибольший смысл, мне просто не нравится идея делать 2 отдельных класса: D мне лень - person Marlon; 07.04.2010
comment
@Marlon: Вы можете обойти необходимость создания 2 отдельных классов, используя 3 (наследуя общие функции, которые у них должны быть во многом из общей базы) :) - person UncleBens; 07.04.2010

К сожалению нет. Оба возвращают один и тот же тип, T*. Рассмотрите возможность использования функций построителя, которые вызывают соответствующий перегруженный конструктор:

template <typename T>
class MyAutoPtr
{
public:
    MyAutoPtr(T* aPtr, bool array = false);
};

template <typename T>
MyAutoPtr<T> make_ptr() {
    return MyAutoPtr<T>(new T(), false);
}

template <typename T>
MyAutoPtr<T> make_ptr(size_t size) {
    return MyAutoPtr<T>(new T[size], true);
}

Теперь вы можете создавать объекты следующим образом:

MyAutoPtr<int> ptr = make_ptr<int>();
MyAutoPtr<int> ptr2 = make_ptr<int>(10);
person Konrad Rudolph    schedule 07.04.2010
comment
Существует способ различать указатели и массивы при передаче в шаблонизированную функцию, но, к сожалению, с точки зрения компилятора, new и new[] соответствуют одному и тому же возвращаемому типу. - person Ramon Zarazua B.; 07.04.2010
comment
@Ramon: можно отличить указатели от массивов, но new[] не создает массив. Массивы в C++ — это только те типы, которые созданы с использованием синтаксиса T[N], где N является константой времени компиляции. - person Konrad Rudolph; 07.04.2010
comment
new[] создает массив. Он просто не возвращает указатель на массив, он возвращает указатель на первый элемент созданного им массива. 5.3.4/1: Если [объект] является массивом, новое выражение возвращает указатель на начальный элемент массива. - person Steve Jessop; 07.04.2010
comment
@Steve: да, плохой выбор слов. Я имел в виду массив в стиле C со статическим типом T[N] (вот что здесь важно). - person Konrad Rudolph; 07.04.2010
comment
Да, самое близкое, что вы можете получить с помощью new[], это получить ссылку на правильный тип, а это означает, что вам нужно знать N во время компиляции. Я предполагаю, что в некоторых случаях N может быть фиксированным, но слишком большим для стека. - person Steve Jessop; 07.04.2010

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

template <class T>
MyAutoPtr<T> make();

template <class T>
MyAutoPtr<T> make(size_t n);

Конечно, это означает, что у вас есть соответствующая логика, но она инкапсулирована. Вы также можете добавить перегрузку, используя T для копирования объекта, переданного во вновь созданный указатель и т.д.

Наконец, это также можно сделать с перегрузками конструктора... дело не в том, чтобы вызывать new снаружи.

person Matthieu M.    schedule 07.04.2010

Я думаю, что реальное решение состоит в том, чтобы избавиться от вашего собственного класса автоуказатель и избавиться от использования массивов в стиле C. Я знаю, что это было сказано много-много раз раньше, но на самом деле больше нет смысла использовать массивы в стиле C. Почти все, что вы можете сделать с ними, можно сделать с помощью std::vector или boost::array. И оба они создают разные типы, так что вы можете перегрузить их.

person Community    schedule 07.04.2010

Это невозможно, так как new int[X] дает указатель на начальный элемент массива. Он имеет тот же тип, что и int*.

Одним из распространенных решений является использование удаления. Добавьте еще один аргумент шаблона в свой класс, чтобы вы могли передать собственный модуль удаления для вашего указателя. Это сделает ваш класс более универсальным. Вы можете создать средство удаления по умолчанию, например следующее:

struct default_deleter
{
    template<typename T>
    void operator()( T* aPtr ) { delete aPtr; }
};

А для массивов вы можете передать настраиваемый модуль удаления:

struct array_deleter
{
    template<typename T>
    void operator()( T* aPtr ) { delete[] aPtr; }
};

Самая простая реализация будет:

template <typename T, typename D>
class MyAutoPtr
{
public: 
    MyAutoPtr(T* aPtr, D deleter = default_deleter() ) : ptr_(aPtr), deleter_(deleter) {};
    ~MyAutoPtr() { deleter_(ptr_); }
protected:
    D deleter_;
    T* ptr_;
};

Затем вы можете использовать его следующим образом:

MyAutoPtr<int, array_deleter> ptr2(new int[10], array_deleter() );

Вы можете сделать свой класс более сложным, чтобы он мог вывести тип для удаления.

person Kirill V. Lyadvinsky    schedule 07.04.2010

new[] специально определяется как значение указателя, несмотря на неявное преобразование массива в указатель, которое в любом случае сработает.

Но я не думаю, что вам не повезло. В конце концов, ваш пример не управляет указателем на int, он управляет указателем на int[10]. Итак, идеальный способ

MyAutoPtr<int[10]> ptr2(new int[10]);

Как упоминает Red-Nosed Unicorn, new int[10] не создает массив C-стиля. Так и будет, если ваш компилятор также соответствует стандарту C, но C++ допускает, чтобы массивы в стиле C были больше, чем массивы в стиле C в C. В любом случае, new создаст вам массив в стиле C, если вы спросите так:

MyAutoPtr<int[10]> ptr2(new int [1] [10]);

К сожалению, delete contents; не будет работать даже с int (*contents)[10];. Компилятору разрешено поступать правильно: в стандарте не указано, что массив преобразуется в указатель, как в случае с new, и, кажется, я припоминаю, что GCC подставил delete[] и выдал предупреждение. Но это неопределенное поведение.

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

template< class T > struct smartptr_dtor {
    void operator()( T *ptr ) { delete ptr; }
};

template< class T, size_t N > struct smartptr_dtor< T[N] > {
    void operator()( T (*ptr) [N] ) { delete [] ptr; }
};

template< class T >
void proper_delete( T *p ) {
    smartptr_dtor< T >()( p );
}

которому по какой-то причине я только что подвергся ;v)

К сожалению, это не работает с массивами динамического размера, поэтому я напишу еще один ответ.

person Potatoswatter    schedule 07.04.2010
comment
@Potatocorn: new int[10] не создает int[10]. Хотя код, который вы написали, можно компилировать, это бессмысленно. Нет никакой связи между типом шаблона и значением конструктора. - person Konrad Rudolph; 07.04.2010
comment
@Red: Почему результат new T[10] не является указателем на T[10]? Тип возвращаемого значения — T*, но он просто не отражает тип объекта в памяти. - person Potatoswatter; 07.04.2010
comment
5.3.4/1: Если сущность не является объектом-массивом, новое выражение возвращает указатель на созданный объект. Если это массив, новое выражение возвращает указатель на начальный элемент массива. - person Potatoswatter; 07.04.2010
comment
@Potatocorn: T[N] — странный зверь в C++, и я не могу ответить на этот вопрос философски. Но на практике есть простая причина: вы не можете присвоить new int[N] T[N]. (GCC говорит incompatible types in assignment of 'int*' to 'int [10]'). Подрыв системы типов в этой точке производит UB. Кроме того, нет такого понятия, как «печатать в памяти». В памяти все байты равны. Типы имеют значение только для компилятора. - person Konrad Rudolph; 07.04.2010
comment
@Potatocorn: 5.3.4/1 здесь просто не применимо. Да, new T[N] создает массив. Но не массив в стиле C типа T[N]. - person Konrad Rudolph; 07.04.2010
comment
@Red: Хм, но new T [1][N] действительно создает и возвращает правильный тип, T(*)[N]. Теперь мне просто нужно бороться с неполным типом T(*)[], который явно не может ссылаться на реальный объект, но тем не менее полезен в качестве параметра шаблона. - person Potatoswatter; 07.04.2010

Вторая попытка…

Довольно легко сделать класс интеллектуальных указателей умным в отношении массивов. Как вы и подозревали, вам не нужен флаг времени выполнения или аргумент для конструктора, если вы знаете, что это массив с самого начала. Единственная проблема заключается в том, что new и new[] имеют идентичные возвращаемые типы, поэтому они не могут передать эту информацию классу интеллектуальных указателей.

template< class T, bool is_array = false >
struct smartptr {
    T *storage;

    smartptr( T *in_st ) : storage( in_st ) {}

    ~smartptr() {
        if ( is_array ) delete [] storage; // one of these
        else delete storage; // is dead code, optimized out
    }
};

smartptr< int > sp( new int );
smartptr< int, true > sp2( new int[5] );

Альтернативой флагу bool является перегрузка значения T[], как, по словам Посетителя, std::unique_ptr делает в C++0x.

template< class T >
struct smartptr {
    T *storage;

    smartptr( T *in_st ) : storage( in_st ) {}

    ~smartptr() { delete storage; }
};

template< class T > // partial specialization
struct smartptr< T [] > {
    T *storage; // "T[]" has nothing to do with storage or anything else

    smartptr( T *in_st ) : storage( in_st ) {}

    ~smartptr() { delete [] storage; }
};

smartptr< int > sp( new int );
smartptr< int[] > sp2( new int[5] );
person Potatoswatter    schedule 07.04.2010