LINUX.ORG.RU

Построить иерархию заголовочных файлов

 ,


2

6

Привет. Друзья, хотелось бы какую-нибудь небольшую утилиту (консольную, без всяких гуёв), которую запускаю в текущей директории, она анализирует все #include в исходниках и выдаёт мне что-то вроде:

               base.h            base2.h
              /     \             |
         child1.h   child2.h     /
                        \       /
                        child3.h
Знаете ли вы что-нибудь такое?

★★

Ответ на: комментарий от RazrFalcon

Я её особо не вертел, но не тяжала ли (вроде кувалдой гвозди забивать), хотелось бы чего-нибудь простого и легковесного. Но как вариант, спасибо.

Раньше я прописывал иерархию в именах файлов: 1_base.h, 1_base2.h, 2_child1.h, 3_child3.h. Но это слишком хлопотно (при перемещении по иерархии какого нибудь базового заголочн, править всех потомков).

pavlick ★★
() автор топика
Последнее исправление: pavlick (всего исправлений: 1)
Ответ на: комментарий от Virtuos86

В зависимости от вкусов или bash + grep, или perl.

В исходниках ядра уже сделали: /usr/src/linux/scripts/headerdep.pl --graph

vodz ★★★★★
()
Ответ на: комментарий от invy

Потому, что «граф» — это титул или математический объект.

ashot ★★★★
()

А зачем, если не секрет? Си (особенно плюсплюс) работает лучше, когда include what you use.

vzzo ★★★
()
Ответ на: комментарий от beastie

Посмотри исходники какого-нибудь более-менее крупного проекта.

anonymous
()

gcc -M, как уже выше сказали. А недохипстеров с doxygen-ом наперевес не слушай

anonymous
()
Ответ на: комментарий от beastie

bad practice

bad practice — думать, что какое-то решение в отрыве от реальности может быть best practice. Та идея с заголовочными файлами, продвигавшаяся Пайком, просто не работает в реальном мире. Она представляет собой неусточивое состояние.

Примерно так же дела обстоят с табами в коде. Не случайно форматтер встроен в компилятор Go (передавай привет Пайку :-) ). Это единственный способ хоть как-то сохранить работоспособность идеи. С заголовками та же проблема. У неё был бы хоть какой-то шанс, будь в единственном компиляторе Си поддержка этой фичи.

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

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

i-rinat ★★★★★
()
Ответ на: комментарий от Deleted

Header files should not be nested. The prologue for a header file should, therefore, describe what other headers need to be #included for the header to be functional. In extreme cases, where a large number of header files are to be included in several different source files, it is acceptable to put all common #includes in one include file.

https://www.doc.ic.ac.uk/lab/cplus/cstyle.html

TL;DR: в общем случае ты полностью теряешь контроль над использумыми библиотеками.

beastie ★★★★★
()
Ответ на: комментарий от invy

Хотел подчеркнуть разницу между иллюстрацией ТСа и тем что есть. Наверное мне следовало использовать звездочки (*)

KennyMinigun ★★★★★
()
Ответ на: комментарий от beastie

Как интересно, а вот тут http://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines#sf11-header-files... написано что заголовки наоборот должны быть self-compilable. Это, как минимум, include в include для базовых классов + весь std, поскольку я не могу предварительно объявлять вещи из std. How so?

d_a ★★★★★
()
Ответ на: комментарий от beastie

Если честно, в первый раз вижу такой подход.

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

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

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

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

+ отвалится функционал многих простых идеешек, т.к они будут считать хедеры невалидными.

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

Deleted
()
Ответ на: комментарий от Deleted

Я придерживаюсь больше гугловских гадлайнов, которые привел d_a

А это и не гугловские гайдлайны, а

collaborative effort led by Bjarne Stroustrup

d_a ★★★★★
()
Ответ на: комментарий от beastie

Хуже практики и код стайла я в жизни не видал.

Dudraug ★★★★★
()
Ответ на: комментарий от beastie

полностью теряешь контроль

Ну, сложностей нет^W возникает немного только при линейных отношениях, а здесь посложнее структура будет. Как при указанном подходе огородить свой код от сторонних интерфейсов(нестабильных) - неясно. Кстати, а нет ли таких, кто ратует за принцип «библиотека не должна компоноваться с другими библиотеками» ?)

Deleted
()
Ответ на: комментарий от beastie

полностью теряешь контроль

Поэтому ты советуешь вообще контроля не иметь.

Deleted
()
Ответ на: комментарий от beastie

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

Но и так очевидно, не надо писать про это мануал.

В остальных случаях это просто «делаем за препроцессор его работу вручную».

Deleted
()
Ответ на: комментарий от Deleted

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

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

Deleted
()
Ответ на: комментарий от Deleted

Платформо-зависимые особенности лучше разруливать системой сборки, а не городить лес из препрцессорных инструкций

Но использовать первое БЕЗ второго, на мой взгляд, задача нетривиальная. Или я не прав?

Deleted
()
Ответ на: комментарий от Deleted

Разные бывают ситуации. Опять же, в идеале, можно практчески обойтись без препроцессора.

Весь платформо-зависимый код обрамляется своими собственными врапперами, которые системой сборки раскрываются в нужные реализации.

Т.е., напр., для работы с файлами в своем приложении создаешь собственный класс.

file.h

#include <string>

std::string readFile(std::string path);

И две реализации: file-win.cpp и file-linux.cpp. В зависимости от платформы сборщик при сборке использует тот или иной сишный файл.

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

Deleted
()
Ответ на: комментарий от Deleted

Ну да, точно. Но ситуации, где всего пара строк из ста отличается, естественней выглядят с препроцессором

Deleted
()
Ответ на: комментарий от Deleted

У гугловцев, зачастую, для таких ситуаций даже сборщики настраивать не надо. В их gyp и golang'овском сборщиках уже по умолчанию есть соглашения о том, что если имя файла попадает под определенную маску, напр., *_win.cpp или *_unix.cpp то они будут использованы только в соответсвенных ОС.

Там правда есть и другие постфиксы, типа _test и какой-то для бенчмарков. Без чтения документации можно долго удивляться странному поведению.

Deleted
()
Ответ на: комментарий от Deleted

Ну да, точно. Но ситуации, где всего пара строк из ста отличается, естественней выглядят с препроцессором

иногда да. Но это опять же вопрос дисциплины и эту часть лучше все же вынести отдельно.

Препроцессор - зло.

Deleted
()
Ответ на: комментарий от Deleted

Препроцессор - зло

Не скажите, под некоторые задачи лучшего нет

Deleted
()

Про gcc -M - это не решает задачу, мы увидим включения в file.h, но не в file.cpp, который может включить потомка и мы получим перекрёст (скомпилируется, но возможен deadlock, например).
В общем удвилён, что нет такой простой и нужной утилиты. Набросал свою, а то надоело следить за цифровыми префиксами исходников.

// 0.1.0
// cat Makefile: g++ -std=c++17 -Wall 1.cpp -lstdc++fs
#include <iostream>
#include <vector>
#include <experimental/filesystem>
#include <fstream>
#include <string>
#include <initializer_list>
#include <regex>
#include <algorithm>

namespace fs = std::experimental::filesystem;
using namespace std;

constexpr initializer_list<const char*> header_ext = {".hpp", ".h"};
constexpr initializer_list<const char*> source_ext = {".cpp", ".c"};

struct Unit
{
  fs::path name;
  vector <const Unit *> deps;
  unsigned hierarchy = -1;
};

int main()
{
   vector<Unit> units;

  for( auto& p: fs::directory_iterator(".") )
    if( fs::is_regular_file(p) )
  	for(const char *head: header_ext)
  	  if(p.path().filename().string().find(head) != string::npos)
  	    {
  	      units.push_back( {p.path().filename()} );
  	      break;
  	    }

  auto find_deps = [&units](Unit &p, ifstream &f)
		   {
		     string buf;
		     std::smatch m;
		     regex reg("[[:space:]]*#[[:space:]]*include[[:space:]]*\"([^/]*)\"");

		     while( getline(f, buf) )
		       if( regex_search( buf, m, std::regex(reg)) )
			 {
			   if( p.name == fs::path(m[1]) )
			     continue;
			   bool found = false;
			   for(Unit &u: units)
			     if(u.name == fs::path(m[1]))
			       {
				 p.deps.emplace_back(&u);
				 found = true;
				 break;
			       }
			   if( ! found )
			     cout << "-----!!!----- INCLUDED FILE "
				  << m[1] << " FROM "
				  << p.name
				  << " IS NOT FOUND\n";
			 }
		   };
  
  for(Unit &p: units)
    {
      ifstream f( p.name.c_str() );
      find_deps(p, f);
      f.close();
	  
      for(const char *src: source_ext)
	{
	  fs::path name_cp = p.name;
	  name_cp.replace_extension(src);
	  f.open( name_cp.c_str() );
	  if( f.is_open() )
	    {
	      find_deps(p, f);
	      break;
	    }
	}
    }

  if(true)
    {
      bool progress;
      do
	{
	  progress = false;
	  for(Unit &p: units)
	    {
	      if(p.hierarchy != (unsigned)-1)
		continue;
	      unsigned pos = 0;
	      for(const Unit *u: p.deps)
		{ 
		  if(u->hierarchy == (unsigned)-1)
		    {
		      pos = -1;
		      break;
		    }
		  if(u->hierarchy+1 > pos)
		    pos = u->hierarchy+1;
		}
	      if(pos != (unsigned)-1)
		{	      
		  p.hierarchy = pos;
		  progress = true;
		}
	    }
	}
      while(progress);
    }

  for(unsigned cur_hier = -1; ; ++ cur_hier)
    {
      vector<string> hier_units;
      for(Unit &p: units)
	{
	  if(p.hierarchy == cur_hier)
	    hier_units.emplace_back( p.name.replace_extension() );
	}
      if( cur_hier != (unsigned)-1  &&  hier_units.empty() )
	break;
      if( ! hier_units.empty() )
	{
	  sort( hier_units.begin(), hier_units.end() );
	  if(cur_hier == (unsigned)-1)
	    cout << "----!!!!!!---- CROSS REFERENCES ----!!!!!!----\n";
	  else
	    cout << "----------------- " << cur_hier << " -----------------\n";
	  for(const string &str: hier_units)
	    cout << str << '\n';
	}
    }

  return 0;
}
Например у на проект:
pavlick@pc ~/ud/test/z $ cat *
//base.h

//child1.cpp
#include "base.h"
#include "child1.h"

//child1.h

//child2.h
#include "base.h"

//child3.h
#include "child1.h"

//child4.cpp
#include "child2.h"
#include "child4.h"

//child4.h

//child5.h
#include "child3.h"
#include "child4.h"
Вывод в консоль:
----------------- 0 -----------------
base
----------------- 1 -----------------
child1
child2
----------------- 2 -----------------
child3
child4
----------------- 3 -----------------
child5

pavlick ★★
() автор топика
Последнее исправление: pavlick (всего исправлений: 2)
Ответ на: комментарий от Deleted

Сравните для того же проекта:

pavlick@pc ~/ud/test/z $ gcc -M child5.h 
child5.o: child5.h /usr/include/stdc-predef.h child3.h child1.h child4.h
Не отображена зависимость ни от base.h, ни от child2.h. А всё потому, что зависимость прослеживается через cpp файл в общей иерархии. Получив такой выхлоп, можно подумать, что base.h может заинклудить child5.h, в итоге будет перекрёст во время исполнения. Представьте, что в child5 мьютекс, он вызывает что-то из base, а тот перенаправляет обратно в child5, в итоге - блокировка. А в той иерархии, которую я построил, файл, находящийся на нижней ступени, не должен включать вышестоящий.

pavlick ★★
() автор топика
Последнее исправление: pavlick (всего исправлений: 1)
Ответ на: комментарий от pavlick

cpp файл в общей иерархии

Во-первых, каким макаром gcc его увидеть должен? Сам cpp же не включается никуда, а в команде он тоже не указан. И вот это:

зависимость прослеживается через cpp файл

Вам таки иерархию заголовков или полный граф по всем исходникам, отражающий отношение включения? Хотя, в любом случае gcc задачу решает: для любого h/cpp будут показаны включения. Алсо, юзате gcc -MM

Deleted
()
Ответ на: комментарий от Deleted

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

Ага, особенно это будет доставлять в случае, когда такой «гаедлайн» используется в какой-нибудь библиотеке. Выходит новая версия и привет - во всех использующих ее проектах надо делать правки :) Писать препроцессорную лапшу, которая будет проверять версию либы и активировать конкретные хидеры. Зато «полный контроль», бггг.

htower_ ★★
()
Ответ на: комментарий от Deleted

Во-первых, каким макаром gcc его увидеть должен? Сам cpp же >>не включается никуда, а в команде он тоже не указан.

А я и не говорил, что он должен. А я своим велосипедом увидел, так что было бы желание. Беру файл.h и анализирую соотвествующий файл.cpp.

Вам таки иерархию заголовков или полный граф по всем >>исходникам, отражающий отношение включения? Хотя, в любом >>случае gcc задачу решает: для любого h/cpp будут показаны >>включения.

Скорее второе, тему можно было бы более удачно назвать, да. GCC задачу не решает, можно спокойно натолкнуться на проблемы. Написать такой пример - элементарно, так что отсутствие перекрёстов лишь в одних заголовках - недостаточное условие.

//1.cpp
#include "2.h"
f1(){
  f2();
}

//1.h
void f1();

//2.h
void f2();

//2.cpp
#include "1.h"
mutex m;
void f2(){
   m.lock();
   f1();
}
Вы можете анализировать это через gcc -MM даже несколько раз, но ошибку не найдёте.

pavlick ★★
() автор топика
Ответ на: комментарий от pavlick
$ gcc -MM -MT2.cpp 2.cpp
2.cpp: 2.cpp 1.h

файл, находящийся на нижней ступени, не должен включать вышестоящий.

reference to upper level detected: 2 -> 1

Если вы об этом. А вообще, подобные ошибки так не найти - selflock в рамках одного уровня как пример

Deleted
()
Ответ на: комментарий от Deleted

reference to upper level detected: 2 -> 1

Что-то не понял, откуда эта строка выплюнулась? Гонял gcc над перекрёстным проектом по всякому, ничего подобного не выдаёт.
В примере выше, проблема даже без мьютекса будет (бесконечная рекурсия). Потенциальный гемор при наличии любых глобальных данных (потеря «атомарности» у операций в рамках одного потока). Следовательно, можно писать как попало, не отслеживая перекрёстов, но придётся отказаться от любых глобальных и статических данных и всё равно иметь риск падения при переполнении стека из-за рекурсии. Дело хозяйское, а меня мой велосипед на 100% устраивает.

pavlick ★★
() автор топика
Ответ на: комментарий от yoghurt

(pimpl не предлагать, но и в этом случае понадобится unique_ptrpolymorphic_value)

anonymous
()
Ответ на: комментарий от beastie

Похоже на первоапрельскую шутку.

anonymous
()
Ответ на: комментарий от i-rinat

идея с заголовочными файлами, продвигавшаяся Пайком

Вот откуда ноги растут! У Пайка какая-то свалка нерабочих идей просто.

rupert ★★★★★
()
Ответ на: комментарий от beastie

А если у меня структура содержит другую структуру (не указатель), то как тогда быть?

SR_team ★★★★★
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.