Code examples and mechanisms

The snippets on this page form a progression: they open a file, look at its schema, read data from it, and then move on to the two mechanisms which are most commonly surprising, the relationship graph and property sets.

Getting Started with IFC parsing

The basis of all parsing and getting information from the IFC starts with obtaining an ifcopenshell::file object and validating that it is good for use. The schema of the file is detected while parsing, and is available as model.schema().

    // The basis of all parsing and getting information from the IFC starts with
    // an ifcopenshell::file, and validating that it is good for use. The path is
    // normally the path to your model, here it is taken from the command line so
    // that this example can actually be run.
    std::string input_file_path = argv[1];
    ifcopenshell::file model(input_file_path);
    if (!model.good()) {
        std::cerr << "Unable to parse .ifc file" << std::endl;
        return 1;
    }

Schema-agnostic parsing of IFCs

It is advisable to design your programme in a schema-agnostic fashion to be able to process all schema versions of input IFCs. In C++ the schema is normally a compile time concept: Ifc4::IfcProduct and Ifc2x3::IfcProduct are generated classes which are unrelated to each other, and the schema libraries you want to support have to be linked.

The runtime part of the API is not tied to a schema though. Instances are express::base values which know their own declaration, and instances_by_type() also accepts the name of an entity as it appears in the IFC schema, so the same code processes IFC2x3, IFC4 and IFC4.3 input.

// The runtime part of the API is not tied to a schema: instances are
// express::base values which know their own declaration, and instances_by_type()
// also accepts the name of an entity as it appears in the IFC schema. Code
// written this way processes IFC2x3, IFC4 and IFC4x3 input alike.
void process_agnostically(ifcopenshell::file& model) {
    for (auto& instance : model.instances_by_type("IfcProduct")) {
        std::cout << instance.declaration().name() << " #" << instance.id();
        // Attributes are read by name, and the value is converted on the fly.
        // get_value() also takes a default for attributes which are optional
        // or which hold something else than what you ask for.
        if (auto product = instance.as<express::entity>()) {
            std::cout << " " << product.get_value<std::string>("GlobalId", "<no guid>");
        }
        std::cout << std::endl;
    }
}

If you do want the strongly typed accessors, the shared logic can be templated over the schema, and the schema dispatched on once.

// If you do want the strongly typed accessors, the shared logic can be
// templated over the schema, and the schema dispatched on once. Note that the
// generated classes are schema specific, so Ifc4::IfcProduct and
// Ifc2x3::IfcProduct are unrelated types, and that the schema libraries you
// want to support have to be linked, see the installation instructions.
template <typename Schema>
void process_typed(ifcopenshell::file& model) {
    for (auto& product : model.instances_by_type<typename Schema::IfcProduct>()) {
        std::cout << product.GlobalId() << std::endl;
    }
}

Reading out attributes of an IfcProduct

The attributes of an IfcProduct, and by extension of any derived class, can be read by calling the accessor of the same name, such as GlobalId() or Name(). Note that optional properties like name, long name, or description, among others, are wrapped in a std::optional, and that properties which reference another instance report as empty - their operator bool returns false - when they are not set.

    // The properties of an IfcProduct, and by extension of any derived class,
    // are read by calling the accessor of the same name: GlobalId(), Name(),
    // and so on. Optional attributes, such as Name and Description, are wrapped
    // in a std::optional. Attributes which reference another instance report as
    // empty - their operator bool returns false - when they are not set.
    auto wall = walls.front();
    std::cout << wall.GlobalId() << std::endl;
    std::cout << wall.Name().value_or("<unnamed>") << std::endl;
    std::cout << wall.Description().value_or("<no description>") << std::endl;
    if (!wall.ObjectPlacement()) {
        std::cout << "<no object placement>" << std::endl;
    }

The same values are available without the generated classes, by index, in the order in which the attributes appear in the IFC schema, or by name. This is what the schema-agnostic code above is built on.

    // The same values are available without the schema classes, by attribute
    // index, in the order in which the attributes appear in the IFC schema,
    // or by name.
    std::cout << static_cast<std::string>(wall.get_attribute_value(0)) << std::endl;
    if (auto product = wall.as<express::entity>()) {
        std::cout << product.get_value<std::string>("Name", "<unnamed>") << std::endl;
    }

Reading properties and quantities from an element

A frequent point of confusion is that IsDefinedBy() does not return the property set or quantity set itself. It is an inverse attribute that lists every IfcRelDefinesByProperties relationship pointing at the element, and the relationship still needs to be unwrapped via RelatingPropertyDefinition() to reach the actual IfcPropertySet (regular properties) or IfcElementQuantity (physical quantities such as length, area, or volume). Both classes derive from IfcPropertySetDefinition, so a single cast check tells you which one you got.

The definition of the relationship is a select, so it is either a single definition or - when the property sets are assigned as a group - a definition set. The value of a property is a select as well, so it is cast to the value type you expect it to hold, which has to be tested with the declaration of the value rather than with the value in a condition:

void print_value(const Ifc4::IfcPropertySingleValue& property) {
    const auto value = property.NominalValue();
    const auto& declaration = value.declaration();
    if (declaration.is(Ifc4::IfcLabel::Class())) {
        std::cout << static_cast<std::string>(value.as<Ifc4::IfcLabel>());
    } else if (declaration.is(Ifc4::IfcIdentifier::Class())) {
        std::cout << static_cast<std::string>(value.as<Ifc4::IfcIdentifier>());
    } else if (declaration.is(Ifc4::IfcReal::Class())) {
        std::cout << static_cast<double>(value.as<Ifc4::IfcReal>());
    } else if (declaration.is(Ifc4::IfcInteger::Class())) {
        std::cout << static_cast<int64_t>(value.as<Ifc4::IfcInteger>());
    } else if (declaration.is(Ifc4::IfcBoolean::Class())) {
        std::cout << (static_cast<bool>(value.as<Ifc4::IfcBoolean>()) ? ".T." : ".F.");
    } else {
        // Measures with a unit, such as IfcThermalTransmittanceMeasure, are
        // defined types over one of the values above, and are printed here as
        // the instance they are.
        property.to_string(std::cout);
    }
}

Both definitions are then handled by the same helper:

// Both IfcPropertySet (regular properties) and IfcElementQuantity (physical
// quantities such as length, area, or volume) are IfcPropertySetDefinition, so
// a single cast tells you which one you got. They are handled separately here
// because their values are reached in different ways.
void print_definitions(const std::vector<Ifc4::IfcPropertySetDefinition>& definitions) {
    for (auto& definition : definitions) {
        if (auto property_set = definition.as<Ifc4::IfcPropertySet>()) {
            std::cout << "  " << property_set.Name().value_or("<unnamed>") << std::endl;
            for (auto& property : property_set.HasProperties()) {
                if (auto single = property.as<Ifc4::IfcPropertySingleValue>()) {
                    // Unlike the optional Name of an IfcRoot, the name of a
                    // property is mandatory.
                    std::cout << "    " << single.Name() << " = ";
                    print_value(single);
                    std::cout << std::endl;
                }
            }
        } else if (auto quantities = definition.as<Ifc4::IfcElementQuantity>()) {
            std::cout << "  " << quantities.Name().value_or("<unnamed>") << std::endl;
            for (auto& quantity : quantities.Quantities()) {
                // Quantity values are in the units of the project, they are not
                // converted to SI units here.
                if (auto length = quantity.as<Ifc4::IfcQuantityLength>()) {
                    std::cout << "    " << length.Name() << " = " << length.LengthValue() << std::endl;
                } else if (auto area = quantity.as<Ifc4::IfcQuantityArea>()) {
                    std::cout << "    " << area.Name() << " = " << area.AreaValue() << std::endl;
                } else if (auto volume = quantity.as<Ifc4::IfcQuantityVolume>()) {
                    std::cout << "    " << volume.Name() << " = " << volume.VolumeValue() << std::endl;
                }
                // IfcQuantityWeight, IfcQuantityCount and IfcQuantityTime follow
                // exactly the same pattern.
            }
        } else {
            // Not every definition is a named set of values: the property
            // definitions of a window or a door type, for example an
            // IfcWindowLiningProperties, are entities with attributes of their
            // own instead.
            definition.to_string(std::cout);
            std::cout << std::endl;
        }
    }
}
    // A frequent point of confusion is that IsDefinedBy() does not return the
    // property set itself. It lists the relationships pointing at the element,
    // and the relationship still has to be unwrapped through
    // RelatingPropertyDefinition() to reach the actual definitions.
    for (auto& relationship : window.IsDefinedBy()) {
        auto definition = relationship.RelatingPropertyDefinition();
        // The definition is a select, because a relationship can also assign a
        // group of definitions at once.
        std::vector<Ifc4::IfcPropertySetDefinition> definitions;
        if (auto single = definition.as<Ifc4::IfcPropertySetDefinition>()) {
            definitions.push_back(single);
        } else if (auto group = definition.as<Ifc4::IfcPropertySetDefinitionSet>()) {
            definitions = static_cast<std::vector<Ifc4::IfcPropertySetDefinition>>(group);
        }
        print_definitions(definitions);
    }

There is a second, easily-missed source of properties: the element’s type. Properties assigned to a type (e.g. a shared “IfcWallType”) apply to every element of that type, and are reached completely differently, through IsTypedBy() and then RelatingType()->HasPropertySets() directly, with no relationship to unwrap:

    // There is a second, easily missed source of properties: the element's
    // type. Properties assigned to, for example, a shared IfcWindowType apply to
    // every element of that type. They are reached in a completely different
    // way, through IsTypedBy() and then straight to HasPropertySets(), with no
    // relationship in between to unwrap.
    auto typed_by = window.IsTypedBy();
    if (!typed_by.empty()) {
        // Unlike IsDefinedBy(), an element is defined by at most one type.
        auto type = typed_by.front().RelatingType();
        if (auto property_sets = type.HasPropertySets()) {
            print_definitions(*property_sets);
        }
    }

Reading a value without the schema types

Casting a value to a schema type requires you to know which of the value types a property holds, and the cast has to be tested before it is used. An attribute can also be read in the type it is stored in, with a visitor over the attribute value, which needs none of the schema level types:

// A value can also be read without knowing any of the schema level value types.
// get_attribute_value() returns an attribute in the type it is stored in, and
// apply_visitor() calls the matching overload of the visitor. A value of
// IfcThermalTransmittanceMeasure arrives as a double here, and the select the
// schema puts around it never enters into it.
struct print_attribute {
    void operator()(bool value) const { std::cout << (value ? ".T." : ".F."); }
    void operator()(boost::logic::tribool value) const {
        // The three valued LOGICAL of the IFC schema, which is unknown when the
        // value is indeterminate.
        std::cout << (boost::logic::indeterminate(value) ? ".U." : (value ? ".T." : ".F."));
    }
    void operator()(int64_t value) const { std::cout << value; }
    void operator()(double value) const { std::cout << value; }
    void operator()(const std::string& value) const { std::cout << value; }
    void operator()(const express::base& value) const {
        // An attribute which holds a value type, such as the
        // IfcThermalTransmittanceMeasure above, is stored as the instance of that
        // type, and the value itself is its first attribute. Anything else, such
        // as a reference to another instance, is printed the way it appears in
        // the file.
        if (value.declaration().as_type_declaration() != nullptr) {
            value.get_attribute_value(0).apply_visitor(*this);
        } else {
            value.to_string(std::cout);
        }
    }
    // Aggregates, binary values, and the placeholders of an incomplete file are
    // not handled here.
    template <typename T>
    void operator()(const T&) const {
        std::cout << "<not a simple value>";
    }
};

void print_stored_value(const Ifc4::IfcPropertySingleValue& property) {
    // NominalValue is the third attribute of an IfcPropertySingleValue.
    property.get_attribute_value(2).apply_visitor(print_attribute{});
}

Which is then used like this:

    // The value of the first property of the first property set, read as the
    // type the file stores it in, without knowing which of the IFC value types
    // is involved. Note that the property sets above were printed by casting to
    // the schema types instead, this is the alternative to that.
    for (auto& relationship : window.IsDefinedBy()) {
        auto property_set = relationship.RelatingPropertyDefinition().as<Ifc4::IfcPropertySet>();
        if (!property_set || property_set.HasProperties().empty()) {
            continue;
        }
        auto property = property_set.HasProperties().front().as<Ifc4::IfcPropertySingleValue>();
        if (property) {
            std::cout << property.Name() << " = ";
            print_stored_value(property);
            std::cout << std::endl;
        }
        break;
    }

Defensive programming with IfcOpenShell

The need for (down-)casting when accessing various properties in an IFC entity is evident from the previous code samples, as the methods and properties usually return the abstract class of the entity, or a select. It is hence important to check for empty values when performing such casts. In C++ that check is the same expression as the cast itself, because a cast which does not apply returns an empty value.

The existence of optional attributes should also be checked, which is what std::optional and the empty reference attributes above are for. A reference which never resolved, such as a STEP ID pointing at an instance which does not exist in the file, is reported the same way rather than throwing.

    // Casting is where things go wrong. A reference attribute holds whatever
    // the file references, which is not necessarily what the schema requires,
    // and an attribute of a supertype only holds what one of its subtypes
    // defines. as<>() returns an empty value when the instance is not of the
    // requested type, and an empty value converts to false, so the cast and its
    // check are a single expression. Never use the result of a cast that was
    // not checked. A reference which never resolved - a STEP ID pointing at an
    // instance which does not exist - is empty as well, and does not throw.
    std::size_t property_sets = 0;
    for (auto& instance : model.instances_by_type("IfcRelationship")) {
        if (auto relationship = instance.as<Ifc4::IfcRelDefinesByProperties>()) {
            auto definition = relationship.RelatingPropertyDefinition();
            // A definition is not always an IfcPropertySet: it can also be an
            // IfcElementQuantity, or a group of definitions.
            if (auto property_set = definition.as<Ifc4::IfcPropertySet>()) {
                ++property_sets;
            }
        }
    }

Note

IfcOpenShell has as of now not been tested explicitly against malicious inputs. Schema validation (the correctness of attribute types and conformance to the rules of the schema) is currently only available in Python, using ifcopenshell.validate --rules, see Validation.