Есть ли * какой-нибудь * способ получить длину массива в стиле C в C ++ / G ++? - PullRequest
8 голосов
/ 16 июня 2011

Я уже давно пытаюсь реализовать функцию length (T * v), но пока безуспешно.

Существует два основных, хорошо известных решения для T v [n] массивов, оба из которых бесполезны или даже опасны после того, как массив был преобразован в указатель T * v.

#define SIZE(v) (sizeof(v) / sizeof(v[0]))

template <class T, size_t n>
size_t lengthof (T (&) [n])
{
    return n;
}

Существуют обходные пути, включающие классы-оболочки и контейнеры, такие как array_proxy STLSoft, boost :: array,std :: vector и т. д. Все они имеют недостатки, и им не хватает простоты, синтаксического сахара и широкого использования массивов.

Существуют мифы о решениях, включающих вызовы, специфичные для компилятора, которые обычно используются компилятором, когдаdelete [] должен знать длину массива.В соответствии с C ++ FAQ Lite 16.14, компиляторы используют два метода для определения объема выделяемой памяти: перераспределение и ассоциативные массивы.При перераспределении он выделяет еще один размер слова и помещает длину массива перед первым объектом.Другой метод, очевидно, хранит длины в ассоциативном массиве.Можно ли узнать, какой метод использует G ++, и извлечь соответствующую длину массива?А как насчет накладных расходов и прокладок?Есть надежда на некомпиляторный код?Или даже не для платформ, встроенных в G ++?

Есть также решения, включающие перегрузку оператора new [] и оператора delete [], которые я реализовал:

std::map<void*, size_t> arrayLengthMap;

inline void* operator new [] (size_t n)
throw (std::bad_alloc)
{
    void* ptr = GC_malloc(n);
    arrayLengthMap[ptr] = n;
    return ptr;
}

inline void operator delete [] (void* ptr)
throw ()
{
    arrayLengthMap.erase(ptr);
    GC_free(ptr);
}

template <class T>
inline size_t lengthof (T* ptr)
{
    std::map<void*, size_t>::const_iterator it = arrayLengthMap.find(ptr);
    if( it == arrayLengthMap.end() ){
        throw std::bad_alloc();
    }
    return it->second / sizeof(T);
}

Это работало хорошо доЯ получил странную ошибку: lengthof не может найти массив.Как оказалось, G ++ выделил на 8 байтов больше в начале этого конкретного массива, чем следовало бы.Хотя оператор new [] должен был вернуть начало всего массива, назовите его ptr, вызывающий код вместо этого получил ptr + 8, поэтому lengthof (ptr + 8), очевидно, потерпел неудачу с исключением (даже если этого не произошло, он мог иметьпотенциально вернул неправильный размер массива).Являются ли эти 8 байтов накладными расходами или заполнением?Не может быть ранее упомянутого перераспределения, функция работала корректно для многих массивов.Что это такое и как отключить или обойти это, предполагая, что можно использовать специфичные для G ++ вызовы или обман?

Редактировать: Из-за многочисленных способов можно выделить C-В массивах стилей, как правило, нельзя определить длину произвольного массива по его указателю, как предположил Оли Чарльзуорт.Но это возможно для неразрушенных статических массивов (см. Функцию шаблона выше) и массивов, выделенных с помощью пользовательского оператора new [] (size_t, size_t), основанного на идее Бена Фойгта:

#include <gc/gc.h>
#include <gc/gc_cpp.h>
#include <iostream>
#include <map>

typedef std::map<void*, std::pair<size_t, size_t> > ArrayLengthMap;
ArrayLengthMap arrayLengthMap;

inline void* operator new [] (size_t size, size_t count)
throw (std::bad_alloc)
{
    void* ptr = GC_malloc(size);
    arrayLengthMap[ptr] = std::pair<size_t, size_t>(size, count);
    return ptr;
}

inline void operator delete [] (void* ptr)
throw ()
{
    ArrayLengthMap::const_iterator it = arrayLengthMap.upper_bound(ptr);
    it--;
    if( it->first <= ptr and ptr < it->first + it->second.first ){
        arrayLengthMap.erase(it->first);
    }
    GC_free(ptr);
}

inline size_t lengthof (void* ptr)
{
    ArrayLengthMap::const_iterator it = arrayLengthMap.upper_bound(ptr);
    it--;
    if( it->first <= ptr and ptr < it->first + it->second.first ){
        return it->second.second;
    }
    throw std::bad_alloc();
}

int main (int argc, char* argv[])
{
    int* v = new (112) int[112];
    std::cout << lengthof(v) << std::endl;
}

К сожалению, из-за произвольных накладных расходов и дополнений компилятором, пока нет надежного способа определения длины динамического массива в пользовательском операторе new [] (size_t), если только мы не предполагаем, что отступ меньше размераодин из элементов массива.

Однако существуют и другие виды массивов, для которых вычисление длины может быть возможным, как предложил Бен Фойгт, поэтому должно быть возможно и желательно создать класс-оболочку, который можетпринимает несколько видов массивов (и их длины) в своих конструкторах и неявно или явно конвертируется в другие классы-обертки и типы массивов.Различные времена жизни разных типов массивов могут быть проблемой, но это можно решить с помощью сборки мусора.

Ответы [ 3 ]

8 голосов
/ 16 июня 2011

Чтобы ответить на этот вопрос:

Есть ли надежда на код, не относящийся к компилятору?

Нет.

В целом, если вы окажетесьЕсли вам нужно сделать это, то вам, вероятно, нужно пересмотреть свой дизайн.Используйте std::vector, например.

4 голосов
/ 16 июня 2011

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

Стандартпозволяет operator new[] украсть несколько байтов для собственного использования, поэтому вместо точного совпадения вам придется выполнять проверку диапазона указателя.std::map, вероятно, не будет эффективным для этого, но отсортированный вектор должен быть (можно искать в двоичном формате).Сбалансированное дерево также должно работать очень хорошо.

1 голос
/ 06 сентября 2013

Некоторое время назад я использовал похожую вещь для отслеживания утечек памяти:

Когда меня попросили выделить размер байт данных, я бы выделил размер + 4 байт и сохраните длину выделения в первых 4 байтах:

static unsigned int total_still_alloced = 0;
void *sys_malloc(UINT size)
{
#if ENABLED( MEMLEAK_CHECK )
  void *result = malloc(size+sizeof(UINT )); 
  if(result)
  {
    memset(result,0,size+sizeof(UINT ));
    *(UINT *)result = size;
    total_still_alloced += size;
    return (void*)((UINT*)result+sizeof(UINT));
  }
  else
  {
    return result;
  }
#else
  void *result = malloc(size);
  if(result) memset(result,0,size);
  return result;
#endif
}

void sys_free(void *p)
{
  if(p != NULL)
  {
#if ENABLED( MEMLEAK_CHECK )
    UINT * real_address = (UINT *)(p)-sizeof(UINT);
    total_still_alloced-= *((UINT *)real_address);

    free((void*)real_address);
#else
    free(p);
#endif
  }
}

В вашем случае получение выделенного размера - это смещение предоставленного адреса на 4 и считывание значения.

Обратите внимание, что если у вас где-то повреждена память ... вы получите неверные результаты.Также обратите внимание, что часто malloc работает внутренне: помещая размер выделения в скрытое поле перед адресом, возвращаемым.На некоторых архитектурах мне даже не нужно выделять больше, достаточно использовать system malloc.

Это агрессивный способ сделать это ... но это работает (при условии, что вы выделяетевсе с этими измененными процедурами размещения, И что вы знаете начальный адрес вашего массива).

...