Note that I'm not saying "don't use 'const'" with the above (and I'm CERTAINLY not saying "using '#define const' in your code base is a good idea"); only that 'const' in C-family languages has significant drawbacks which may affect the engineering tradeoffs you make when you decide whether to use it.
By contrast, languages such as Ada have explicit and implicit constantness restrictions ('in' parameters, for instance, cannot be modified), but these do not modify the type of the object, allowing objects with these restrictions to be type-compatible with objects without them.
AFAIK this is pretty much limited to C and C++. Many (and I am willing to venture most, or at least the most-used) C-family languages that aren't C/C++ don't suffer this issue. Go and Javascript, for example, both use "const" in a way that doesn't modify the type of the underlying object.
Although it's been a long time since I coded in C/C++, correct me if I'm wrong.
In the C++ Working Paper, the Standardese is N4527 5.2.2 [expr.call]/4 "When a function is called, each parameter (8.3.5) shall be initialized (8.5, 12.8, 12.1) with its corresponding argument." followed by 8.5 [dcl.init] where /17.8 has a nice example:
"Note: An expression of type "cv1 T" can initialize an object of type "cv2 T" independently of the cv-qualifiers cv1 and cv2. int a; const int b = a; int c = b;"
(A "cv-qualifier" is const or volatile; they behave similarly in many contexts, hence the collective terminology for convenience.)
Assuming you do it from the start you shouldn't be running in to issues.
But from my experience const really helps in enforcing directed graph constraints on logic and data flow which is great - and forces code to be easier to reason about.
What's more, it can "infect" any other code that touches it, in turn requiring casting or weird const additions. For a simple rant, check the first few paragraphs here:
You can't just declare something a constant then cast it out of being constant later? Of course not, that's the entire point!
Or any of several other things, including some truly odd possibilities using operator and function overloading.
It's just really gross as implemented.
And this is 'const''s fault? Others have asked - can you provide an example? Because what you're describing sounds like it's going to be a hot mess, const or no const.
I don't really think that const can make bad data representation any worse than it already is.
std::vector<char*> vec1(...);
std::vector<char const*> vec2(...);
std::copy(vec1.begin(), vec1.end(), vec2.begin()); // works (1)
std::copy(vec2.begin(), vec2.end(), vec1.begin()); // fails (2)
If this compiled, the second example would invoke undefined behavior and crash if you attempted to write to an element that pointed to read-only memory (e.g., a string literal).Container element types are almost never declared const unless their values are truly immutable (in this case the pointer is mutable, but the underlying memory is not, which is kind of strange).
...
std::vector<char*> vec1(...);
foo(vec1); // fails
The idea is that you should be allowed to pass non-const vector to const function who accepts same vector (const or not const) with const variables. Because the function foo is like saying "give me vector, I will not modify it's elements", and with current C++ standard this is not allowed.If you're writing a program, you should use the same type, std::vector<char* >, everywhere.
If you're writing a library, the API should avoid including container details in function parameters, and const and non-const T should be allowed.
template<typename Container>
void foo1(Container const& c);
template<typename Iterator>
void foo2(Iterator first);
template<typename Iterator>
void foo3(Iterator first, Iterator last);
template<typename Iterator>
void foo4(Iterator first, std::size_t num);
template<typename T>
void foo5(std::vector<T> const& vec);
void foo6(char const* const* first, std::size_t num);
foo6 is a pretty good alternative here, imo: std::vector<char*> vec(...);
foo6(&vec.front(), vec.size());
It would be cool if the compiler could figure templated type conversion out, but the crux of the issue is, as I understand it, that T<A> and T<A const> are not necessarily implemented the same way.Also note that std::vector<std::string const> won't compile -- it wouldn't be copy-assignable -- so these issues mostly seem to arise specifically when dealing with C strings and/or C compatibility. And reinterpret_cast is amazingly helpful for porting from C.
void blah(vector<const char * > &xs) {
xs.push_back("blah");
}
And you have the code calling it: vector<char * > xs;
blah(xs);
And that would not be valid. The rationale for the conversion rules for pointers to pointers is based on a similar sort of problem. (In fact I think it's exactly the same problem, at any level, but that only just occurred to me, so perhaps not.)(This isn't to say there aren't other similar sorts of conversion that would be safe, and that the language perhaps should support, just that this isn't one of the valid ones.)
I'm not even sure I understand the rant, though I've been a fan of const for years so it's probably automatic by now. So, say you have a class with a const member, and you have to pass it in to a function? Well, that's a bit unusual - const members are pretty rare in my experience. But the underlying problem sounds like: you have a const object, and you want to pass it into a function. There are three possibilities, I suppose:
1. The function takes const T & or const T * , because it doesn't change that argument. So no problem.
2. The function takes T, because it doesn't change that argument (and the object is small and/or the programmer doesn't care about the copying - irrelevant issues here). So no problem.
3. The function takes T & or T * , because it changes that argument. Compilation fails! But - no problem! Because this is what you want. You passed what's supposedly a const object into a function that expected to be able to change it. Obviously compilation has to fail.
So maybe the problem is people declaring parameters as T & or T * , when they meant const T & or const T * ?
I bet if C++ had const by default, this problem would never arise. We'd be striding, right now, arm in arm, towards our glorious const-filled destiny, singing a song about it as we went.
(References, by contrast, can never be reseated. So const T &const doesn't make sense. But... I didn't try this right now to double check, so if it works - though I think it doesn't - don't blame me. Or, if you do, at least allow me to plead extenuating circumstances, in the form of C++.)
'const' in C and C++ is broken. Not only is it like a Chinese word that varies wildly in meaning depending on where you use it and how it is inflected, but in C-family languages, 'const' modifies type, making objects that benefit from the compiler hints 'const' implies type-incompatible with objects that do not.
They're not type-incompatible in any way that doesn't violate those compiler hints in the first place - C and C++ fully support sensible coercion between const and non-const types. What more do you want?
This results in the problem of some know-it-all deciding that a core API needs to be const-correct, and checks in changes making it so, only to find that the entire fucking code base is now broken
Would you rather it compiled and silently broke? If some dingus breaks the build by changing an API without checking in the corresponding code using the API, how is that C/C++'s fault?
By contrast, languages such as Ada have explicit and implicit constantness restrictions ('in' parameters, for instance, cannot be modified), but these do not modify the type of the object, allowing objects with these restrictions to be type-compatible with objects without them.
This is bizarre. Can you take that 'in' parameter and then pass it on to another function as an 'in/out' parameter? If so, then either
1. Ada just made you a copy silently (bad)
2. Ada is broken and 'in' means nothing (worse).
If neither of those are true and an 'in' can't be transitively passed on as an 'in/out', then that's a type - however you want to refer to it, 'in' is then a type with all of the 'broken' type restrictions you're railing against in C/C++.
Suppose you want to write a function that returns all the nodes in a tree that satisfy some property. In C++ you have to define two different versions (or use templates to make the compiler clone it for you):
vector<Node*> GetGoodNodes(Node* root);
vector<const Node*> GetGoodNodes(const Node* root);
If more than one type is involved in the operation, there can be a combinatorial explosion.Does this function return data point that it creates itself, or does it retrieve these nodes from somewhere else? If it's the first option, who's in charge of deleting them? If it's a second option, who's in charge of counting references to these objects? Also, if it's a second option, and this data points are retrieved from a certain storage, are you absolutely sure that whoever gets this data should be able to modify them?
It returns pointers to the nodes in the tree. You know this because the caller isn't supposed to (or allowed to) delete them, because if the caller was responsible for that, they'd be wrapped in smart pointers.
> If it's a second option, who's in charge of counting references to these objects?
The pointers in the return value are valid as long as their particular descendant of the function parameter isn't deleted. There's no need for reference counts, and if they're present at all, they aren't updated by this function, since the caller isn't responsible for decrementing them. If the caller was responsible, the return value would be a vector of smart pointers.
> Also, if it's a second option, and this data points are retrieved from a certain storage, are you absolutely sure that whoever gets this data should be able to modify them?
If they have a const node *, they shouldn't. If they don't, they should.
And your final answer is, well, exactly the point I was trying to steer the conversation to. In other words, if you declare both methods, it means that you're doing something wrong: you're giving away data mutably and immutably at the same time, and it just doesn't make sense.
If you have a non-const node, you can access its children non-constly _or_ constly (which you could do anyway, since you could make your reference to the node be const at any time). If you have a const node, you can only access its children constly.
This makes perfect sense.
Yeah, come to think of it if there's a chance this function came from a pre-C++11 codebase I wouldn't be so sure that the caller wasn't supposed to delete those nodes. Well, it'd be an odd thing for this particular function, I guess, but it's something I've seen or might have done, in general. In that case maybe I'd have had the function take an output parameter instead, in order to give it a scary variable name.
Whenever I need to return a non-const reference/pointer to some inner data (which is not often) I name the method "mutableFoo()" to make clear what you're getting.
But most of the time (provided a sane API design) you won't have that problem anyway.
If 'in' is a type than so is the concept of an lvalue in C and C++ -- because constants (and 'in' parameters which function as constants with local scope) in Ada are not lvalues; they cannot be assigned to by the semantics of the language. (In C and C++, every sense of 'const' can be gotten around; even const methods can mutate object instance variables by const_casting this.) You can call any constraint imposed by the language a type and that may be true in the abstract sense, but what counts when talking about types in a language is what the language's own type system has to say on the matter.
Parameter modes (in, out, in out) form a part of the type of the subprogram (procedure or function) the parameter is a part of, but constantness is not a part of the type of the value in Ada. Note that this is not the only way types differ in Ada than in C++ when it comes to access: C++ types encode information about how a parameter is passed, Ada types do not. The Ada declaration:
procedure Foo(I : in Integer);
may be equivalent to C++ void foo(int i);
or void foo(const int& i);
depending on how the compiler chooses to pass the values. Note that even 'in out' parameters may be passed by value through a 'pass by copy-in copy-out' mechanism, so whether a parameter is passed by value or reference is largely handled behind the scenes, so the type system doesn't include that information. If it did, Ada would be even more verbose and unwieldy than its reputation suggests, as type enforcement is much more strict in Ada.What counts isn't so much what the specification calls a 'type' but rather how the compiler does type reduction - and with all of the compilers I've worked with, argument decorators like 'in' & 'out' are typically handled during type reduction.
depending on how the compiler chooses to pass the values. Note that even 'in out' parameters may be passed by value through a 'pass by copy-in copy-out' mechanism, so whether a parameter is passed by value or reference is largely handled behind the scenes, so the type system doesn't include that information. If it did, Ada would be even more verbose and unwieldy than its reputation suggests, as type enforcement is much more strict in Ada.
Very interesting! I know very little about Ada - other than that it has generally been considered as a safer alternative to C for applications-programming in the aerospace software world.
At the same time, I can say that without the ability to determine the how of how the compiler will pass arguments - by value, by copy, by reverse copy, by reference - I wouldn't consider Ada to be an alternative to C in the systems-programming domain, any more than I would consider Haskell or Javascript to be systems-programming languages. Your original claim, in your words, was that 'const' in C and C++ is broken. C, or any other systems-programming language, simply has more constraints on what const could possibly be. The Ada approach to handling const would simply never work in C with C's implementation guarantees. To pseudo-quote a great man, Dennis Ritchie,
If you want magical argument passing, sometimes resulting in silent and unexpected argument copying, you know where to get it.
It was fixed years ago. Now const behaves properly in modern C compilers.
I work on a large, mainly Java codebase. If we used `final` everywhere it is applicable, the code would have a ton of visual noise. We find that `final` is not as valuable in most cases.
`final` doesn't buy you as much in Java as `const` does in C, or (the lack of) `mut` in Rust.
In Rust, I can pass in an object reference without mut, and I know that the object will be the exact same after the function call.
In Java, even if a function takes `final ArrayList<..> xs`, the function can still delete every element of your array, modify elements at will, etc.
That means `final` is basically useless as far as using an API goes (I don't care if the implementation of a function reassigns to the same variable as the object I'm passing in, but I do care if they mutate it).
`final` only helps you avoid dumb reassignment bugs that tests should probably catch anyway, and in that case, if you are already writing good tests, why pollute the code with the visual noise of `final`?
(Obviously you know that, Steve, but it's the sort of thing that I find a lot of Java folks mentally don't get.)
As for "visual noise," I find it helps to highlight variable declarations and results in more readable code, but we're firmly in the personal opinion area here.
* Member variables of a class that should be initialized in the constructor
* Static final constants
* Any configuration-like variable
* Like you mentioned, when you need to initialize a variable in a particular scope, but the initialization could happen in several control paths of inner scopes
* Function parameters.
Re-assigning a variable used as a function parameter is frequently a mistake, and almost always makes code more confusing. I find the "this is defined only once in this function" philosophy to be useful in keeping complexity down in Java programs. Even if final doesn't make objects immutable.
Quite true.
> `final` only helps you avoid dumb reassignment bugs that tests should probably catch anyway ...
There is another reason for declaring parameters `final` in Java. It is the only way[1] which they can be used in an anonymous class definition such as:
return new SomeClass () {
override int addOne () {
return aNamedOuterMethodParam + 1;
}
}
1 - "The only way" as of Java 7. Newer versions may have removed this restriction, I haven't bothered to check.If you are working with code where you know that functions never modify their parameters then it is simpler to reason about what it does. I prefer to write code this way myself if there is no language mechanism to enforce it.
Other possible uses of `final` (to tag methods or classes) have the downside of reducing the flexibility of code by removing interfaces / "seams" that can be overridden. This reduction of flexibility will generally make things harder to test, which may or may not matter. Michael Feathers has a short article discussing this [1].
[1] Michael Feathers' "Testable Java" : http://www.objectmentor.com/resources/articles/TestableJava....
For built-ins, such as numeric types and "char" (String is obviously disallowed from this definition), final will prevent changes to instances of those types similar to what "const int" does for C++.
For pointers, however, the final keyword in Java limits the ability to change the variable holding the pointer, not to what it references. The equivalent in C++ would be:
SomeClass * const foo = &instanceOfSomeClass;
Here, the pointer "foo" could not change, but operations changing foo would be allowed.Java (the language) does not possess the semantics to express "the pointer given to a method cannot allow changes to what it points."
I saw a couple of his introductory videos and I think he is great to teach specifics about game programming, but should definitely not teach how to write C.
See http://duramecho.com/ComputerInformation/WhyHowCppConst.html
If I wrote a library and made something const, I did that for a reason. I didn't do that just to piss you off.
The most common usage of const in library code is for function parameters:
Iterator find(T const& value) const;
The const actually _helps_ callers: find accepts both const and non-const references. A non-const argument will be implicitly cast to const by the compiler.Compare that to:
Iterator find(T& value) const;
This function signature accepts only non-const references (unless T is const). A caller must const_cast a const argument to use it (bad).Return values are occasionally references:
T& front();
T const& front() const;
But "auto" makes the code shorter when the return type is const or unknown: auto& v = c.front(); // overload is deduced by the compiler based on the constness of c.1. I don't like the syntax for const [fair enough]
2. I want to have an object that obeys someone's const'ing of it but still be able to mess around with its internal state (like for logging, data caching, or intermediate calculations), and there's no way of doing that...
3. ...except that there is - it's the mutable keyword, but that's bad because other programmers other than me who are using it to modify 'const'ed classes (for logging, data caching, or intermediate calculations) can't be trusted.
Seriously - that's what he says:
One in particular annoys me because my programs often needed to be optimized for speed. [because I'm 'leet] This is that a method which is declared ‘const’ cannot even make changes to the hidden parts of its object that would not make any changes that would be apparent from the outside
...
In later versions of C++, the ‘mutable’ keyword was added which enables ‘const’ to be overridden for this purpose but it totally relies on trusting the programmer to only use it for that purpose so
!!!