8
$\begingroup$

Some languages have a concept of classes separately than structs. In C++ the only difference is whether the members are public or private by default. This seems redundant. Why have a separate class keyword for such a minor and trivial difference? In Swift the difference is whether they are reference types or value types.

Is there an advantage to include both in cases where the difference is so minor for example in C++? How else does or could class behave differently than a struct in other languages that would warrant having them separately?

$\endgroup$
7
  • 3
    $\begingroup$ There are almost unlimited ways that classes and structs "could" behave differently, but most of them wouldn't be useful. Perhaps it would be better if the question asked about what differences exist between classes and structs in other languages which have both. $\endgroup$
    – kaya3
    Jun 30, 2023 at 2:21
  • 6
    $\begingroup$ I think it seems kind of weird to first come up with a feature (having both class and struct, with a distinction), then try to retroactively find a reason to have that feature. $\endgroup$ Jun 30, 2023 at 2:28
  • 6
    $\begingroup$ Ultimately, class and struct are two arbitrary keywords you can have in a programming language. They don’t universally refer to any well-defined semantic concept, so this question is ill-defined. Pedantically, the answer to “could class behave different from struct?” is “yes, because they can behave absolutely however the language designer decides they should behave.” $\endgroup$
    – Alexis King
    Jun 30, 2023 at 3:08
  • 2
    $\begingroup$ The answers to this question seem to demostrate it is valuable. $\endgroup$ Jun 30, 2023 at 7:30
  • 1
    $\begingroup$ @BruceAdams I agree, I originally voted to close and then to reopen because while the focus of the question is weird, in some other (non-C++) languages classes and structures are very different beasts and defining what exactly is a class or a structure could be very interesting. $\endgroup$
    – kouta-kun
    Jun 30, 2023 at 15:37

7 Answers 7

8
$\begingroup$

Not Necessarily

Lots of modern languages agree with you, which is why we're seeing the rise of the concept called a data class. Basically, rather than having two completely disjoint concepts for "OOP-style classes" and "data structures", we start with a full-featured notion of a class and then optionally add-on some functionality to make it behave more like a data structure when we want.

In Kotlin, the keyword is data class. In Java, it's record. In Python, it's a decorator called @dataclass. And in Scala, which arguably pioneered the concept, it's called a case class. The key point, in each of these cases, is that the thing we're talking about is still a class; it's just a class with a lot of extra behavior thrown in.

Common functionality included in this feature:

  • Public fields, or automatically-generated getters for each field.
  • A constructor which takes values for all of the public fields as arguments.
  • A generated toString() which prints out all of the fields.
  • Structural equality and hashing.
  • A copy method which produces a new instance of the class, with a small number of fields changed.
$\endgroup$
1
  • 1
    $\begingroup$ And then you have C# which has class and struct and recently added record class and record struct, which may be a (ahem) record number of structures to express classes. $\endgroup$ Jul 1, 2023 at 4:17
6
$\begingroup$

Keep in mind C++ has a heavy C inheritance. A class defined as struct in a C++ program, if it has no special features like virtual functions, is indistinguishable in memory from a PODS (Plain Old Data Struct) and having all of their members be publicly accessible as if being defined in a C program is all the better in that sense.


If thinking of a new, C++ style language, the difference between struct and class could be to directly not allow any OOP features in a struct, for example member methods, inheritance etc, making structs essentially a C compatibility feature. It would also allow this language to do its' own thing with member ordering in the new style classes (enforce a special padding/alignment, add metadata, etc) while maintaining the expected structure ordering as defined by the platform's ABI in POD structures. The sky's the limit when you don't have to think about backwards compatibility like C++ does.

$\endgroup$
5
$\begingroup$

For the most part, this distinction tends to reflect the history and evolution of languages.

Structures are a simple feature that most languages have had since they were originally designed. They existed in early languages such as PL/I and COBOL. They were also added easily to other languages, e.g. DEFSTRUCT in Lisp (DEFSTRUCT was originally a macro defined in a popular user-written library).

Classes are a more advanced feature that came later, mainly from object-oriented languages such as Smalltalk.

Over time, many of the older languages adopted OO features, including classes. Or, as in C++ and Objective C, they evolved into new languages. But structures were kept in the language for backward compatibility.

There's obvioiusly considerable overlap in their functionality. But even in C++, where the only technical difference is the default visibility of members (private vs public), programmers tend to conceptualize them differently. Structures tend to be used when you just need a simple container with named slots, while classes are used when you need OO features such as inheritance or polymorphism.

This usually makes programs easier to understand. When you see a structure definition, you can infer that you don't have to worry about fancy methods associated with it, or subclass relationships.

$\endgroup$
4
$\begingroup$

If the difference really is very small then there does, as you say seem little purpose in distinguishing. Sometimes the difference may be historical. But the difference may not be small.

Common Lisp is an example of a language where there are two sorts of class-like thing which are quite distinct. In CL's case the difference is both historical and justified by the semantic differences.

As originally defined, CL allowed you to define structures using the macro defstruct. Structures:

  • have named fields, which can have defined types and init forms;
  • have accessors predefined for these fields;
  • support single-inheritance which might be done by inclusion;
  • usually are a distinct type although there is a special hack which allows defstruct to define 'structures' which are just lists or vectors, which is useful to wrap old code;
  • where a type is defined there will be appropriate subtype relationships;
  • and various other useful things.

Structures in particular may not be portably redefined, so code can assume things about accessors: they can be inlined, type inference is possible &c.

Since structures are (by default) distinct types which inherit from any parent structure type it's possible to build a simple object system using them.

In the final (so far) version of CL, CLOS, the Common Lisp Object System appeared, and with it classes defined with defclass. These classes:

  • have named slots ...
  • ... which may or may not have readers, writers, or accessors (both), as well as other attributes;
  • support multiple inheritance;
  • are distinct types (see below);
  • there are appropriate subtype relationships;
  • support redefinition;
  • and many other useful things.

Critically classes support redefinition: you can redefine a class and the behaviour in this case is specified. Existing instances of the class get updated and you can intervene in this process in a well-defined way. So for example if you remove a slot from a class you can write code which defines what happens in that case. You can modify the inheritance graph as part of redefinition and this works.

CLOS also defines many other fancy features for classes defined with defclass.

CLOS does two and a half other things.

  • Classes and types are largely unified: any class corresponds to a type, and many types have corresponding classes. In particular the structure types defined by defstruct have corresponding classes.
  • Methods are not defined in classes but rather belong to generic functions, and may specialize on more than one argument. Redefinition of methods is of course supported, as is the addition of new methods to existing generic functions. Automatically-defined slot access functions are methods on generic functions, so you can use the whole machinery of generic functions with them.
  • Additionally a hairy definition of CLOS itself in terms of itself, the metaobject protocol, was not standardized, but many CL implementations have more-or-less compatible versions of it.

The end result of this is that there are two families of classes:

  • structure types are probably fast but limited, although you can use most of the machinery of generic functions with them (structure field accessors are not methods on generic finctions though);
  • classes defined with defclass are enormously flexible, with the full generic function machinery available, but probably both slower and larger.

Although this tradeoff is partly historical, it's a good compromise in general.

$\endgroup$
3
$\begingroup$

If we declare that the construct has no methods, private fields (so no hidden internal state) or inheritance, this has the same effect as using bool as a synonym for int in C-like languages, or keywords like const, final, std:: and others similar. There is more context what it is, helping with readability.

It is possible to use an object, instance of the class, for everything. Or int, char as boolean, or string as int. This is just less readable.

$\endgroup$
1
  • 1
    $\begingroup$ Welcome! I made an edit which I think clarifies your intent, because at first I didn't understand that part of your answer. Please feel free to revert my edit or make a different one if I got it wrong. $\endgroup$
    – kaya3
    Jun 30, 2023 at 13:56
3
$\begingroup$

Depends on the (runtime) semantic difference

I think it depends on whether the semantic difference is big enough.

The distinction between private and public is meaningless, it's not a runtime semantic.

It makes sense to distinguish between value types and reference types, whose memory structure and runtime semantics are different.


My Design and Distinguish

Allow me to introduce the difference between structure and class in valkyrie language here.

class is an out-of-the-box structure, suitable for daily use.

And structure is a professional, lower-level structure with more configurations, so it is not recommended for daily use.

Data Layouts

class use auto layout, auto arrange fields, auto fill alignment

structure use manually layout, the field order is equal to the writing order, and needs to be filled and aligned by itself.

Auto Traits

Many grammars in the valkyrie language are related to traits, and classes will automatically implement these traits.

class Point {
    x: int,
    y: int,
}
# from `Constructor::construct`
Point(1, y: 2)
# from `Extractor::extract`
case Point(x, y): print("({x}, {y})")
# from `Equal::equal`
a == b || a != b
# from `Encode::encode`
p.encode::(Json)()

And structure have to be implemented by themselves.

Auto Casts

In the valkyrie language, subtypes need to implement cast to supertypes.

class uses two private fields to store the parent class by default.

structure does not generate anything automatically, you need to implement cast manually.

class C(B1, B2) { }

structure C(B1, B2) {
    private _b1: B1,
    private _b2: B2,
}
imply C: Cast(B1) {
    cast(self): B1 {
        self._b1;
    }
}
imply C: Cast(B2) {
    cast(self): B2 {
        self._b2;
    }
}

This mechanism is used to solve the situation where virtual base classes, repeated base classes, or staggered base classes can get better layout.

$\endgroup$
2
$\begingroup$

In many situations, it's useful to have a means of specifying that a group of storage "fastened together with duct tape" should be usable as though it were a single storage location. It may also be useful to have a means of implicitly creating a wrapper object that can hold such a group, or extracting the contents thereof.

Some programmers view such things as being some weird broken form of object, rather than recognizing them for what they are: groups of storage locations held together with duct tape.

In C#, if one has types:

class pointClass {public int x,y; };
struct pointStruct { public int x,y; };

along with an array of each type, the semantics of the struct will often be much clearer than those of the class. For example, suppose one has an array of pointClass whose first element is a reference to a pointClass holding values 1,2, and one wants the first element of the array to hold a reference to a pointClass instance with values 1,3. There are three ways this might happen:

  1. Replace the first element of the array with a reference to a new pointClass object which holds values 1,3.

  2. Modify the y field of the pointClass object to which the first element of the array holds a reference.

  3. Identify some instance of pointClass which holds 1,3, and will never be accessible by code that might try to never modify it, and store a reference to that within the array.

Any of these might be the right approach, depending the application, but it's not always clear which one would be appropriate. Selecting the wrong one may yield hard-to-track-down bugs.

If instead of holding a pointClass, the array had held an exposed-field pointStruct, then there would be no question about what should happen. The second int values in the first element of the array should be changed to 3. Since no "reference" to those values could exist outside the array, the question of whether the first element of the array identifies the same pointStruct instance as anything else in the universe would be simple to answer: the only references of any sort that could exist to the first element of the array would be a scope-limited "byref"; any byref that was created to identify that array element would throughout its lifetime identify that same element, and could not be detached from it, and any that wasn't thuus created could never identify the same element.

$\endgroup$
1
  • $\begingroup$ @kaya3-supportthestrike: Thanks. My main point is to emphasize the "group of storage locations stuck together with duct tape". While a class object can kinda sorta mimic a group of storage locations stuck together with duct tape, I find myself annoyed at people who view structs as an inferior form of class, and think structs should try to imitate a class trying to imitate a bunch of storage locations stuck together with duct tape, rather than simply having a struct act like what it actually is. $\endgroup$
    – supercat
    Jul 1, 2023 at 5:42

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .