Skip to content

C++ datamodel v1.0#7534

Draft
aothms wants to merge 64 commits intov0.8.0from
datamodel-v1.0
Draft

C++ datamodel v1.0#7534
aothms wants to merge 64 commits intov0.8.0from
datamodel-v1.0

Conversation

@aothms
Copy link
Copy Markdown
Member

@aothms aothms commented Jan 5, 2026

For the past couple of days I have been rethinking what a sane v1.0 data model on the C++ side would look like that balances safety, readability, performance and a bit more modern language features. I still have work todo but wanted to share a bit of what's going on.

This has huge implications on client C++ code, so to feel the pain I went through most of the code base already. @RickBrice as one of the more active C++ devs lately I'm curious what your thoughts are.

Outcome:

>>> import ifcopenshell
>>> f = ifcopenshell.file(schema='ifc4x3_add2')
>>> f.createIfcWall()
#1=IfcWall($,$,$,$,$,$,$,$,$)
>>> inst = f[1]
>>> f.remove(f[1])
>>> inst
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "C:\Users\tkrij\miniconda3\envs\ifopsh08\lib\site-packages\ifcopenshell\ifcopenshell_wrapper.py", line 5999, in __repr__
    return _ifcopenshell_wrapper.entity_instance___repr__(self)
RuntimeError: Trying to access deleted instance reference

Before:

	IfcSchema::IfcOpenShell* shell = new IfcSchema::IfcOpenShell(faces);
	IfcSchema::IfcConnectedFaceSet::list::ptr shells(new IfcSchema::IfcConnectedFaceSet::list);
	shells->push(shell);
	IfcSchema::IfcFaceBasedSurfaceModel* surface_model = new IfcSchema::IfcFaceBasedSurfaceModel(shells);

	IfcSchema::IfcRepresentation::list::ptr reps(new IfcSchema::IfcRepresentation::list);
	IfcSchema::IfcRepresentationItem::list::ptr items(new IfcSchema::IfcRepresentationItem::list);

	items->push(surface_model);

	IfcSchema::IfcShapeRepresentation* rep = new IfcSchema::IfcShapeRepresentation(
		0, std::string("Facetation"), std::string("SurfaceModel"), items);

	reps->push(rep);
	IfcSchema::IfcProductDefinitionShape* shapedef = new IfcSchema::IfcProductDefinitionShape(boost::none, boost::none, reps);

	return shapedef;

After:

    auto shell = f.create<IfcSchema::IfcOpenShell>();
    shell.setCfsFaces(faces);

    auto surface_model = f.create<IfcSchema::IfcFaceBasedSurfaceModel>();
    surface_model.setFbsmFaces({shell});

	auto rep = f.create<IfcSchema::IfcShapeRepresentation>();
    rep.setRepresentationIdentifier("Tessellation");
    rep.setRepresentationType("SurfaceModel");
    rep.setItems({surface_model});

	auto shapedef = f.create<IfcSchema::IfcProductDefinitionShape>();
    shapedef.setRepresentations({rep});

	return shapedef;

Rationale

reason for weak_ptr encapsulated in object:

  • safety and matches semantics (the instance pointers always had their lifetime scoped to the file - this is now explicit and enforced)
  • objects can cast, so variants (EXPRESS SELECT) can work with implicit casts instead of virtual inheritance
  • elimination of virtual methods might help performance (although the declaration* is essentially the same mechanism) and reduce dependency on rtti in case that ever becomes a concern
  • elimination of virtual methods unifies latebound and earlybound entity definitions
  • which means it's possible now to do geometry interpretation on late bound schemas (previously not possible because of dependency on dynamic_cast / rtti)
  • cleaner separation between early bound statically typed layer and attribute storage container (which was a bit muddy in earlier releases but essentially present)
  • ability to redeclare the type of entity instances by assigning a new declaration* which was previously not possible as it was baked into the typeid

reason for creation as part of file

  • might be more efficient than create() + add()
  • necessary for rocksdb anyway, because file context is necessary for attribute storage in key-value store
  • instances not stored in file leak easily and have missing behaviour regarding inverses which depend on the file maps

reasons for no constructor:

  • readability because no named args
  • easier to adapt to multiple schemas
  • anyway immutability is not an option
  • reduce binary size
  • one way to do things
  • don't like argument forwarding because is created as part of file now

no custom class anymore for aggregates (aggregate_of_instance)

  • we can use aggregate initializers now and stl features
  • but casts are a bit uglier


uint32_t express::Base::id() const { return data()->id(); }

const InstanceData* express::Base::data() const {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method, as well as the non-const version, defeats using smart pointers. Once the raw pointer is returned it is no longer managed and could be deleted by the caller. Why not

auto sp = data_.lock();
if (sp) {
   return sp;
} else {
   throw std::runtime_error("Trying to access deleted instance reference")
}

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's true, but I have the feeling that maybe this should be marked private instead and then it doesn't really matter anymore.

@RickBrice
Copy link
Copy Markdown
Contributor

RickBrice commented Jan 6, 2026

Does the weak_ptr encapsulated in the express::Base retain file scope lifetime of objects? It seems like shared_ptr would be required. Though, I don't have a complete understanding.

Creating entities as part of the file without constructors has a trade-off. Instead of create() + add() a user of the SDK must do create() + multiple parameter initialization steps by calling set() methods. This isn't bad, but it has these consequences:

  • Multiple set() methods must be invoked instead of a single constructor
  • Constructors had all parameters needed for proper construction and initialization. Now the caller needs to know which set() methods to call for all of the required initialization parameters. Failure (or forgetting) to initialize required parameters will likely lead to unwanted behavior.
  • Perhaps consider adding an initialize() method that has all the parameters of the old constructors. They the process is obj=f.create() + obj.initialize().

I very much like eliminating the custom classes for aggregates. I found them difficult to use. It was difficult to know which one of the classes where needed in different situations. Their interface wasn't the same as std collections, so looping wasn't as easy and they couldn't be used in the std algorithms.

This is going to break a lot of my code, but the fixes appear to be easy to deal with.

@aothms Let's set up a time for you to walk me through the bigger picture. I'm sure there are lots of details that I'm missing.


// Disable warnings coming from IfcOpenShell
#pragma warning(disable : 4018 4267 4250 4984 4985)
/********************************************************************************
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like a different example has been applied over the IfcAlignment.cpp example. The revised file is significantly different. Is this intentional?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sorry that was sloppy, it's back and updated for the code changes

@aothms
Copy link
Copy Markdown
Member Author

aothms commented Jan 7, 2026

Does the weak_ptr encapsulated in the express::Base retain file scope lifetime of objects? It seems like shared_ptr would be required. Though, I don't have a complete understanding.

Yes the file have the shared_ptr not so much for sharing ownership but just so that we have the option to have a weak_ptr derived from them. So that when you have instances pointing to a deleted file or to instances from the file that have been deleted.

Now the caller needs to know which set() methods to call for all of the required initialization parameters.

Yes that's very valid. The discovery is indeed not really there.

I'm also fine with forwarding the arguments from file.create and reinstate the constructor. But as I said since the arguments are not named I find them a bit unclear especially with all the std::nullopts for all the optional arguments, it's hard to interpret. Boost has this named parameters idea, but I think it's a bit lengthy to setup https://www.boost.org/doc/libs/latest/libs/graph/doc/bgl_named_params.html

Perhaps consider adding an initialize() method that has all the parameters of the old constructors. They the process is obj=f.create() + obj.initialize().

I think that's indeed also a nice compromise. Then we also have autocompletion in our IDE because I think with std::forward from file.create() I think IDE's will be pretty clueless.

Very flexible to have a call these days, let me know some times that suit you please.

@RickBrice
Copy link
Copy Markdown
Contributor

I emailed your personal account with some proposed times for next week

@aothms
Copy link
Copy Markdown
Member Author

aothms commented Jan 15, 2026

@RickBrice The initialize() method is there. There are now three options:

        // 1.
        auto rel = file.create<IfcSchema::IfcRelVoidsElement>();
        rel.setGlobalId(guid());
        rel.setOwnerHistory(file.getSingle<IfcSchema::IfcOwnerHistory>());
        rel.setRelatingObject(roof);
        rel.setRelatedObjects({south_roof_part, north_roof_part});

        // 2.
        file.create<IfcSchema::IfcRelAggregates>(
            guid(), 
            file.getSingle<IfcSchema::IfcOwnerHistory>(), 
            std::nullopt, 
            std::nullopt, 
            roof, 
            std::vector<::Ifc2x3::IfcObjectDefinition>{south_roof_part, north_roof_part}
        );

        // 3.
        file.create<IfcSchema::IfcRelAggregates>().initialize(
            guid(),
            file.getSingle<IfcSchema::IfcOwnerHistory>(),
            std::nullopt,
            std::nullopt,
            roof,
            {south_roof_part, north_roof_part});

For option 2 I couldn't get the untyped initializer list to work. I'm not so familiar with them.

Maybe maybe if we have initialize() return *this again, it's also not necessary to do argument forwarding from create(), because then you can do file::create<T>().initialize(args...) and have the created and initialized instance returned in one anonymous statement.

@Andrej730 @Moult I think it's also a good time for the both of you to have a look. I'm getting to a point that the tests are almost passing. I still need to do quite a bit of cleaning up. I think quite some docstrings and annotations disappeared in the process of moving some things to the SWIG generated classes, I will try and get them back. Things feel quite a bit snappier without the wrapped_data indirections and the constant decorate-undecorate when setting and getting attributes. But I didn't yet find time to test this.

std::distance(declaration().as_entity()->derived().begin(), it),
Derived{}
);
void InstanceData::populate_derived_() {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there is a lot of getting of the begin and end iterator. Probably not a big deal for any one entity, but over thousands of entities in a model, would it be more efficient to do something like this?

if (auto* ent = declaration_->as_entity()) {
   auto begin = ent->derived().begin();
   auto end = ent->derived().end();
   for ( auto it = begin; it != end; ++it) {
      if (*it) {
         set_attribute_value(
            std::distance(begin,it),Derived{}):
      }
   }

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I can't imagine this would matter with compiler optimizations, but would be good to check the disassembly.

@RickBrice
Copy link
Copy Markdown
Contributor

@aothms

I'm not as knowledgeable on C++ as you, but from a little googling, there does seem to be an untyped initializer list in c++. The closest thing I could find is

#include <iostream>
#include <initializer_list>
#include <any>
#include <typeinfo>

void printAnyList(std::initializer_list<std::any> values) {
    for (const auto& v : values) {
        if (v.type() == typeid(int))
            std::cout << std::any_cast<int>(v) << " ";
        else if (v.type() == typeid(double))
            std::cout << std::any_cast<double>(v) << " ";
        else if (v.type() == typeid(const char*))
            std::cout << std::any_cast<const char*>(v) << " ";
    }
    std::cout << "\n";
}

int main() {
    printAnyList({1, 2.5, "hello"}); // works with mixed types
}

This does not seem like a workable solution.

Returning *this from initialize() seems like a good idea. Then you can do something like:

        auto myrelagg = file.create<IfcSchema::IfcRelAggregates>().initialize(
            guid(),
            file.getSingle<IfcSchema::IfcOwnerHistory>(),
            std::nullopt,
            std::nullopt,
            roof,
            {south_roof_part, north_roof_part});

@RickBrice
Copy link
Copy Markdown
Contributor

RickBrice commented Jan 16, 2026

Another thing comes to mind, but not related to initialization. Not sure how to say it generically, so I'll do it with an example. To add a Related Object to IfcRelAggregates after it is constructed you need to do the following

auto rel = file.create<IfcRelAggregates>().initialize(...);
auto related_objects = rel.RelatedObjects(); // causes a vector copy
related_objects.push_back(new_related_object);
rel.setRelatedObjects(related_objects); // causes another vector copy

Consider the following

class IfcRelAggregates : public...
{
public:
   const std::vector<::Ifc4x3_add2::IfcObjectDefinition>& RelatedObjects() const; // no copy and constness is preserved
   void addRelatedObject(const ::Ifc4x3_add2::IfcObjectDefinition& relobj); // relobj can be added into the internal storage without copying the vector of related objects twice
};

Before:

        auto myrel = file.create<IfcSchema::IfcRelAggregates>().initialize(
            guid(),
            file.getSingle<IfcSchema::IfcOwnerHistory>(),
            std::nullopt,
            std::nullopt,
            roof,
            {south_roof_part, north_roof_part});
       auto relobjs = myrel.RelatedObjects();
      relobjs.push_back(east_roof_part);
      myrel.setRelatedObjects(relobjs);

After:

        auto myrel = file.create<IfcSchema::IfcRelAggregates>().initialize(
            guid(),
            file.getSingle<IfcSchema::IfcOwnerHistory>(),
            std::nullopt,
            std::nullopt,
            roof,
            {south_roof_part, north_roof_part});
       myrel.addRelatedObject(east_roof_part); // easy to add... could also be myrel.addRelatedObjects({east_roof_part,west_roof_part});

      auto relobjs = myrel.RelatedObjects(); // no copy
     for(auto relobj : relobjs) {
        relobj.Name(); // do something interesting here
     }

The same concept could be applied to all of the different Rel types, and other things with lists like IfcSectionedSolidHorizontal.CrossSections

@Moult
Copy link
Copy Markdown
Contributor

Moult commented Jan 27, 2026

I hope I understand what's happening here, to me it means:

  • No more segfault on accessing removed element to solve Graceful handling of instance deletion in python #4637
  • No more wrapped_data (I assume anywhere we currently use wrapped_data in the code needs to change), in exchange for a general all round performance increase when accessing (direct? inverse?) attributes on entity instances?
  • Smaller binary due to no constructors so faster WASM loading?
  • ... anything else?

@RickBrice
Copy link
Copy Markdown
Contributor

@aothms what is the current state of this?
I’m developing more and more on the C++ side and it’s all going to break with this update. I would like to minimize the pain.

@aothms
Copy link
Copy Markdown
Member Author

aothms commented Mar 1, 2026

Regarding this comment below. Yes, this is something that has always been incredibly wasteful. Also on the python side. And quite problematic because there are a lot of 1-to-many objectified relationships where the many are sometimes really many many. Like the ContainedInStructure relationship in your typical building. Beyond building storey there is typically very little decomposition so in a typical bad case you can literally have a million nodes into a single aggregate attribute, where this copying really starts to exhibit some nasty non-linear behaviour if you're incrementally adding things to it.

Since this is also on the python-side. I'd prefer to solve this in the attribute value and not in the schema (python doesn't have acces to the schema). That means re-introducing some custom vector-alternative that has more context (needs to know entity instance and attribute index essentially) so that it can update the inverse relationships also when doing a push_back (or an erase).

Previously this was hardly possible at all, because the vectors returned from the attribute storage where constructed on the fly. Now there is at least a vector type in our storage variant.

Maybe this also means we can stop with casting-by-copying of the vector types, because this is also really silly. We store them as untyped vector and then create a copy of vector with element type of a specific subtype and do some static or dynamic cast on the elements. But we can also do this lazily with a custom iterator.

Becomes even more complex when we have to consider vector<vector<>> but that is really an exception maybe not worth to much time.

I certainly think this is something worth exploring.

template <typename T>
class ifcopenshell::vector {
  class custom_iterator_over_this_thing {};

  Entity owner;
  size_t attribute_index;

  custom_iterator_over_this_thing begin() const;
  custom_iterator_over_this_thing end() const;

  void push_back(const T& t) {
    // add to values
    // update inverses
  }
};

Another thing comes to mind, but not related to initialization. Not sure how to say it generically, so I'll do it with an example. To add a Related Object to IfcRelAggregates after it is constructed you need to do the following

auto rel = file.create<IfcRelAggregates>().initialize(...);
auto related_objects = rel.RelatedObjects(); // causes a vector copy
related_objects.push_back(new_related_object);
rel.setRelatedObjects(related_objects); // causes another vector copy

Consider the following

class IfcRelAggregates : public...
{
public:
   const std::vector<::Ifc4x3_add2::IfcObjectDefinition>& RelatedObjects() const; // no copy and constness is preserved
   void addRelatedObject(const ::Ifc4x3_add2::IfcObjectDefinition& relobj); // relobj can be added into the internal storage without copying the vector of related objects twice
};

Before:

        auto myrel = file.create<IfcSchema::IfcRelAggregates>().initialize(
            guid(),
            file.getSingle<IfcSchema::IfcOwnerHistory>(),
            std::nullopt,
            std::nullopt,
            roof,
            {south_roof_part, north_roof_part});
       auto relobjs = myrel.RelatedObjects();
      relobjs.push_back(east_roof_part);
      myrel.setRelatedObjects(relobjs);

After:

        auto myrel = file.create<IfcSchema::IfcRelAggregates>().initialize(
            guid(),
            file.getSingle<IfcSchema::IfcOwnerHistory>(),
            std::nullopt,
            std::nullopt,
            roof,
            {south_roof_part, north_roof_part});
       myrel.addRelatedObject(east_roof_part); // easy to add... could also be myrel.addRelatedObjects({east_roof_part,west_roof_part});

      auto relobjs = myrel.RelatedObjects(); // no copy
     for(auto relobj : relobjs) {
        relobj.Name(); // do something interesting here
     }

The same concept could be applied to all of the different Rel types, and other things with lists like IfcSectionedSolidHorizontal.CrossSections

@aothms
Copy link
Copy Markdown
Member Author

aothms commented Mar 1, 2026

@aothms what is the current state of this? I’m developing more and more on the C++ side and it’s all going to break with this update. I would like to minimize the pain.

You decide:

  • Forget about this for now
  • Prioritize and merge asap
  • Let is percolate a bit, but at least make it somewhat stable so that you can work off of it

@RickBrice
Copy link
Copy Markdown
Contributor

Since this is also on the python-side. I'd prefer to solve this in the attribute value and not in the schema (python doesn't have access to the schema). That means re-introducing some custom vector-alternative...

I liked the idea of using STL without something special. I didn't think about dealing with inverses. The fundamental question is then, how much of an actual problem is this? Is it worth the effort to solve it?

You decide:
Forget about this for now

It seems like we both have other things going on that are higher priority. We can forget about it for now. While there will be pain on the C++ side, it doesn't appear to be unbearable.

@aothms
Copy link
Copy Markdown
Member Author

aothms commented Mar 3, 2026

I liked the idea of using STL without something special. I didn't think about dealing with inverses. The fundamental question is then, how much of an actual problem is this? Is it worth the effort to solve it?

Yes me too, but on the other hand STL is designed to work with custom containers because everything accepts iterators. I think people in a well-designed library would expect to be able to do relationship.RelatedObjects().push_back(something_new). Since anyway we're giving ourselves some time I will see what the implications are.

While there will be pain on the C++ side, it doesn't appear to be unbearable.

I think if we have file.create<Something>().initialize() the pain will be quite regexable.. hopefully..

@theoryshaw theoryshaw marked this pull request as draft March 14, 2026 16:14
@theoryshaw
Copy link
Copy Markdown
Member

Converted to a draft, as it currently conflicts with v0.8.0.

see https://github.com/falken10vdl/bonsaiPR/releases/tag/v0.8.5-alpha2603141648

please note the First detected failing date is most likely wrong, since that was the date this automated conflict report functionality was 'turned on'.

Conflict Report:

  • PR #7534: C++ datamodel v1.0
    • Reason: Fails to merge against base (problem with PR itself)
    • First detected failing: 2026-03-10
    • Base commit at first detection: 972a1fd3091d172de30c539c6423e87f5a3ac2db
    • Conflicting files: src/examples/IfcAdvancedHouse.cpp, src/examples/IfcOpenHouse.cpp, src/ifcgeom/infra_sweep_helper.cpp, src/ifcgeom/kernels/opencascade/OpenCascadeKernel.cpp, src/ifcgeom/kernels/opencascade/loft.cpp, src/ifcgeom/kernels/opencascade/loop.cpp, src/ifcgeom/mapping/IfcAxis2PlacementLinear.cpp, src/ifcgeom/mapping/IfcObjectPlacement.cpp, src/ifcgeom/mapping/IfcOpenCrossProfileDef.cpp, src/ifcgeom/mapping/IfcRevolvedAreaSolid.cpp, src/ifcgeom/mapping/IfcSectionedSolidHorizontal.cpp, src/ifcgeom/mapping/IfcSectionedSurface.cpp, src/ifcgeom/mapping/IfcSurfaceOfRevolution.cpp, src/ifcgeom/mapping/IfcToroidalSurface.cpp, src/ifcgeom/mapping/mapping.cpp, src/ifcgeom/taxonomy.cpp, src/ifcgeom/taxonomy.h, src/ifcopenshell-python/ifcopenshell/__init__.py, src/ifcopenshell-python/ifcopenshell/api/project/append_asset.py, src/ifcopenshell-python/ifcopenshell/express/header.py, src/ifcopenshell-python/ifcopenshell/express/implementation.py, src/ifcopenshell-python/ifcopenshell/express/mapping.py, src/ifcopenshell-python/ifcopenshell/express/rule_compiler.py, src/ifcopenshell-python/ifcopenshell/express/rule_executor.py, src/ifcopenshell-python/ifcopenshell/express/rules/IFC2X3.py, src/ifcopenshell-python/ifcopenshell/express/rules/IFC4.py, src/ifcopenshell-python/ifcopenshell/express/rules/IFC4X1.py, src/ifcopenshell-python/ifcopenshell/express/rules/IFC4X2.py, src/ifcopenshell-python/ifcopenshell/express/rules/IFC4X3.py, src/ifcopenshell-python/ifcopenshell/express/rules/IFC4X3_ADD1.py, src/ifcopenshell-python/ifcopenshell/express/rules/IFC4X3_ADD2.py, src/ifcopenshell-python/ifcopenshell/express/rules/IFC4X3_RC1.py, src/ifcopenshell-python/ifcopenshell/express/rules/IFC4X3_RC2.py, src/ifcopenshell-python/ifcopenshell/express/rules/IFC4X3_RC3.py, src/ifcopenshell-python/ifcopenshell/express/rules/IFC4X3_RC4.py, src/ifcopenshell-python/ifcopenshell/express/rules/IFC4X3_TC1.py, src/ifcopenshell-python/ifcopenshell/express/schema_class.py, src/ifcopenshell-python/ifcopenshell/express/templates.py, src/ifcopenshell-python/ifcopenshell/file.py, src/ifcopenshell-python/ifcopenshell/geom/main.py, src/ifcopenshell-python/ifcopenshell/sql.py, src/ifcopenshell-python/ifcopenshell/stream.py, src/ifcopenshell-python/ifcopenshell/util/element.py, src/ifcopenshell-python/ifcopenshell/util/pset.py, src/svgfill/src/arrange_polygons.cpp, win/build-deps.cmd
    • Possible breaking commits: 43533706a examples - build IfcOpenHouse/IfcAdvancedHouse using cmake package, 5a0671584 Update examples, 1e4693346 fix project name for IfcAdvancedHouse, c78b2893d Proceed with merge, a85571979 Merge branch 'v0.8.0' into v0.8.0-merge

@Moult
Copy link
Copy Markdown
Contributor

Moult commented Mar 28, 2026

I managed to get this to compile and import in Python locally mostly by letting AI work it out without any real understanding. Not sure if it's useful or not, but I've attached the findings.

findings.md

@Moult
Copy link
Copy Markdown
Contributor

Moult commented Mar 28, 2026

I experienced segfaults when doing ifcopenshell.open() (after all those compilation fixes in the previous comment) on the ISSUE_159_kleine_Wohnung_R22.ifc model (from the list of profiling models). Here's an updated AI generated findings.md with an explanation of that fix, and a patch showing all the changes so far.

I'm also getting iterator initialisation errors on the advanced_model.ifc model saying "Trying to access deleted instance reference" (with detailed logging turned on). Haven't yet found a solution to that yet though.

Other than that, with those patches I can compile, and the profiling code is able to basically recreate the same numbers (apart from that initialisation error on that model) as the main branch.

ai.patch
findings.md

@aothms
Copy link
Copy Markdown
Member Author

aothms commented Mar 28, 2026

Hm yes apparently openai codex only bothered to verify compilation on windows. These template/typename things are notoriously different between compiler implementations.

@Moult
Copy link
Copy Markdown
Contributor

Moult commented Mar 28, 2026

Woke up this morning and the "Trying to access deleted instance reference" was solved by AI with this explanation:

findings.md
ai.patch

@aothms
Copy link
Copy Markdown
Member Author

aothms commented Mar 29, 2026

Thanks! that fix we probably cannot use, because that means that every deleted instance equals every other deleted instance. We want this generally to bubble up as an exception (I think). But the root cause is super useful we just need to catch this a bit earlier, because apparently this is not a very common issue so I don't think it warrants a fix so high up in the abstraction level.

I have a bit of a backlog of other work because I missed a good couple of days due to back issues, but I'll get to this later this week. If you want to have it chew at some other tasks in the meantime, essentially:

  • ifcgeom/mapping/mapping.cpp: taxonomy::ptr mapping::map(const express::Base& inst)

    Don't loop over all mappable geometry types for every converted instance, but rather "compile" based on IfcParse::declaration* into a function table in the mapping constructor so that we can dispatch to the appropriate call in sub linear time. Take care of subtypes: entity subtypes should be handled by the most closely defined handler in the inheritance tree. This for example applies to profiles where some subtypes are handled by their parent profile handler and some have a distinct handler for a specific subtype.

  • src/ifcgeom/AbstractKernel.h: dispatch_conversion.

    Same idea, but maybe more complex. Here we do not have a #include based loop, but a recursive template-based one. I always expected the compiler to optimize this into some sort of jump table, but it doesn't seem it happens.

  • Add a third "PassThrough" geometry kernel that only accepts taxonomy::shell with faces with at most 4 linear edges (so that triangulation is trivial) or extrusions of polygonal bases without openings. That writes the taxonomy::shell essentially directly to the IfcGeom::Representation::Triangulation, tie this in too hybrid kernel so that we can call something like hybrid-passthrough-cgal-simple-opencascade. Make the shape and structure similar to the occt and cgal kernel.

  • Create a third paramatrization of the cgal geometry lib. Now we have cgal and cgal-simple which corresponds to Exact_predicates_exact_construction (necessary for Nef) Exact_predicates_inexact_construction (prevents some crashes). But actually the idea was that cgal-simple would correspond to Simple_cartesian<double> which is much lighter, but less robust. If we at least have access to all three parametrizations we can better understand the failure modes the real cgal-simple and maybe start using it again.

  • (low prio) for cgal non-simple implement an alternative boolean logic routine not based on Nef_3, but on CGAL/Polygon_mesh_processing/corefinement

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants